From d95beb45c5adfd3604335da60c41db472a3e5ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chojnacki?= Date: Tue, 14 Jul 2026 19:53:24 +0200 Subject: [PATCH 1/5] Add RUM offset write regression coverage --- .../InjectingPipeOutputStreamTest.groovy | 27 ++++++++++++++ .../buffer/InjectingPipeWriterTest.groovy | 26 ++++++++++++++ .../application/build.gradle | 1 + .../controller/FormResponseController.java | 36 +++++++++++++++++++ .../resources/templates/form-response.html | 16 +++++++++ .../SpringBootWebmvcIntegrationTest.groovy | 31 +++++++++++++++- 6 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/java/datadog/smoketest/springboot/controller/FormResponseController.java create mode 100644 dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/resources/templates/form-response.html diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index fcf4699075d..959b3ea6b9b 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.bootstrap.instrumentation.buffer import datadog.trace.test.util.DDSpecification +import java.util.concurrent.atomic.AtomicInteger import java.util.function.LongConsumer class InjectingPipeOutputStreamTest extends DDSpecification { @@ -152,6 +153,32 @@ class InjectingPipeOutputStreamTest extends DDSpecification { downstream.toByteArray() == testBytes.getBytes("UTF-8") } + def 'should honor non-zero offsets in bulk writes: #scenario'() { + setup: + def prefix = "ignored-prefix" + def payloadBytes = payload.getBytes("UTF-8") + def source = (prefix + payload + "ignored-suffix").getBytes("UTF-8") + def downstream = new ByteArrayOutputStream() + def counter = new Counter() + def injections = new AtomicInteger() + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES, injections.&incrementAndGet, { long bytes -> counter.incr(bytes) }, null) + + when: + piped.write(source, prefix.getBytes("UTF-8").length, payloadBytes.length) + piped.close() + + then: + downstream.toByteArray() == expected.getBytes("UTF-8") + injections.get() == expectedInjections + counter.value == payloadBytes.length + + where: + scenario | payload | expected | expectedInjections + "without a marker" | "safe" | "safe" | 0 + "with a marker" | "dynamicsafe" | "dynamicsafe" | 1 + "with a marker at the end" | "dynamic-content" | "dynamic-content" | 1 + } + def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: def testBytes = "test content".getBytes("UTF-8") diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy index 7466839f7c8..840742b6d5e 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.bootstrap.instrumentation.buffer import datadog.trace.test.util.DDSpecification +import java.util.concurrent.atomic.AtomicInteger import java.util.function.LongConsumer class InjectingPipeWriterTest extends DDSpecification { @@ -168,6 +169,31 @@ class InjectingPipeWriterTest extends DDSpecification { downstream.toString() == testBytes } + def 'should honor non-zero offsets in bulk writes: #scenario'() { + setup: + def prefix = "ignored-prefix" + def source = (prefix + payload + "ignored-suffix").toCharArray() + def downstream = new StringWriter() + def counter = new Counter() + def injections = new AtomicInteger() + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS, injections.&incrementAndGet, { long bytes -> counter.incr(bytes) }, null) + + when: + piped.write(source, prefix.length(), payload.length()) + piped.close() + + then: + downstream.toString() == expected + injections.get() == expectedInjections + counter.value == payload.length() + + where: + scenario | payload | expected | expectedInjections + "without a marker" | "safe" | "safe" | 0 + "with a marker" | "dynamicsafe" | "dynamicsafe" | 1 + "with a marker at the end" | "dynamic-content" | "dynamic-content" | 1 + } + def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: def downstream = new StringWriter() diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/application/build.gradle b/dd-smoke-tests/spring-boot-3.3-webmvc/application/build.gradle index 1b6aae09b00..0ab5e0481a4 100644 --- a/dd-smoke-tests/spring-boot-3.3-webmvc/application/build.gradle +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/application/build.gradle @@ -24,6 +24,7 @@ java { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation group: 'com.h2database', name: 'h2', version: '2.1.214' compileOnly group:"com.google.code.findbugs", name:"jsr305", version:"3.0.2" diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/java/datadog/smoketest/springboot/controller/FormResponseController.java b/dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/java/datadog/smoketest/springboot/controller/FormResponseController.java new file mode 100644 index 00000000000..7dc045b0aac --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/java/datadog/smoketest/springboot/controller/FormResponseController.java @@ -0,0 +1,36 @@ +package datadog.smoketest.springboot.controller; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Locale; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; + +@Controller +public class FormResponseController { + private static final int RESPONSE_OFFSET = 6; + + private final SpringTemplateEngine templateEngine; + + public FormResponseController(SpringTemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + @GetMapping("/form-response") + public void formResponse(HttpServletResponse response) throws IOException { + Context context = new Context(Locale.ROOT); + context.setVariable("returnUrl", "https://app.example.test/flows/complete?request=request-123"); + context.setVariable("formAction", "https://provider.example.test/flows/continue"); + context.setVariable("requestId", "request-123"); + + String content = templateEngine.process("form-response", context); + // Exercise response writers that receive a slice of a reusable buffer. + char[] responseBuffer = new char[RESPONSE_OFFSET + content.length()]; + content.getChars(0, content.length(), responseBuffer, RESPONSE_OFFSET); + + response.setContentType("text/html"); + response.getWriter().write(responseBuffer, RESPONSE_OFFSET, content.length()); + } +} diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/resources/templates/form-response.html b/dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/resources/templates/form-response.html new file mode 100644 index 00000000000..ba05b5c3467 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/application/src/main/resources/templates/form-response.html @@ -0,0 +1,16 @@ + + + + + Continue request + + + +
+ +
+ + diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy index 362a365a00f..c3e7c9b6f04 100644 --- a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy @@ -14,6 +14,10 @@ class SpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { command.add(javaPath()) command.addAll(defaultJavaProperties) command.addAll((String[]) [ + "-Ddd.rum.enabled=true", + "-Ddd.rum.application.id=appid", + "-Ddd.rum.client.token=token", + "-Ddd.rum.remote.configuration.id=12345", "-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter", "-jar", springBootShadowJar, @@ -32,7 +36,8 @@ class SpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { protected Set expectedTraces() { return [ "\\[servlet\\.request:GET /fruits\\[spring\\.handler:FruitController\\.listFruits\\[repository\\.operation:FruitRepository\\.findAll\\[h2\\.query:.*", - "\\[servlet\\.request:GET /fruits/\\{name}\\[spring\\.handler:FruitController\\.findOneFruit\\[repository\\.operation:FruitRepository\\.findByName\\[h2\\.query:.*" + "\\[servlet\\.request:GET /fruits/\\{name}\\[spring\\.handler:FruitController\\.findOneFruit\\[repository\\.operation:FruitRepository\\.findByName\\[h2\\.query:.*", + "\\[servlet\\.request:GET /form-response\\[spring\\.handler:FormResponseController\\.formResponse.*" ] } @@ -79,6 +84,30 @@ class SpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { waitForTraceCount(1) } + def "inject RUM without corrupting a Thymeleaf form response"() { + setup: + String url = "http://localhost:${httpPort}/form-response" + + when: + def response = client.newCall(new Request.Builder().url(url).get().build()).execute() + + then: + response.code() == 200 + response.header("x-datadog-rum-injected") == "1" + def responseBodyStr = response.body().string() + responseBodyStr.contains('const returnUrl = "https:\\/\\/app.example.test\\/flows\\/complete?request=request-123";') + responseBodyStr.contains('window.formResponse = { returnUrl: returnUrl };') + responseBodyStr.contains('onload="document.getElementById(\'response-form\').submit()"') + responseBodyStr.contains('action="https://provider.example.test/flows/continue"') + responseBodyStr.contains('value="request-123"') + responseBodyStr.count("DD_RUM.init(") == 1 + responseBodyStr.contains("https://www.datadoghq-browser-agent.com") + responseBodyStr.count("") == 1 + responseBodyStr.indexOf("DD_RUM.init(") < responseBodyStr.indexOf("") + responseBodyStr.trim().endsWith("") + waitForTraceCount(1) + } + @Override List expectedTelemetryDependencies() { ['spring-core'] From ea2c639e494edab4e09f80a16711aa43540a7649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chojnacki?= Date: Tue, 14 Jul 2026 19:54:07 +0200 Subject: [PATCH 2/5] Fix RUM injection for offset writes --- .../buffer/InjectingPipeOutputStream.java | 10 +++++----- .../instrumentation/buffer/InjectingPipeWriter.java | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index 7221a85d007..ba117d659aa 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -137,7 +137,7 @@ public void write(byte[] array, int off, int len) throws IOException { // we have a full match. just write everything filter = false; drain(); - int bytesToWrite = idx; + int bytesToWrite = idx - off; downstream.write(array, off, bytesToWrite); bytesWritten += bytesToWrite; long injectionStart = System.nanoTime(); @@ -149,8 +149,8 @@ public void write(byte[] array, int off, int len) throws IOException { if (onContentInjected != null) { onContentInjected.run(); } - bytesToWrite = len - idx; - downstream.write(array, off + idx, bytesToWrite); + bytesToWrite = off + len - idx; + downstream.write(array, idx, bytesToWrite); bytesWritten += bytesToWrite; } else { // we don't have a full match. write everything in a bulk except the lookbehind buffer @@ -167,7 +167,7 @@ public void write(byte[] array, int off, int len) throws IOException { downstream.write(array, off + marker.length - 1, bytesToWrite); bytesWritten += bytesToWrite; filter = wasFiltering; - for (int i = len - marker.length + 1; i < len; i++) { + for (int i = off + len - marker.length + 1; i < off + len; i++) { write(array[i]); } } @@ -180,7 +180,7 @@ public void write(byte[] array, int off, int len) throws IOException { } private int arrayContains(byte[] array, int off, int len, byte[] search) { - for (int i = off; i < len - search.length; i++) { + for (int i = off; i <= off + len - search.length; i++) { if (array[i] == search[0]) { boolean found = true; int k = i; diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index 8d2c222d924..00f2e8541f4 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -137,7 +137,7 @@ public void write(char[] array, int off, int len) throws IOException { // we have a full match. just write everything filter = false; drain(); - int bytesToWrite = idx; + int bytesToWrite = idx - off; downstream.write(array, off, bytesToWrite); bytesWritten += bytesToWrite; long injectionStart = System.nanoTime(); @@ -149,8 +149,8 @@ public void write(char[] array, int off, int len) throws IOException { if (onContentInjected != null) { onContentInjected.run(); } - bytesToWrite = len - idx; - downstream.write(array, off + idx, bytesToWrite); + bytesToWrite = off + len - idx; + downstream.write(array, idx, bytesToWrite); bytesWritten += bytesToWrite; } else { // we don't have a full match. write everything in a bulk except the lookbehind buffer @@ -168,7 +168,7 @@ public void write(char[] array, int off, int len) throws IOException { bytesWritten += bytesToWrite; filter = wasFiltering; - for (int i = len - marker.length + 1; i < len; i++) { + for (int i = off + len - marker.length + 1; i < off + len; i++) { write(array[i]); } } @@ -181,7 +181,7 @@ public void write(char[] array, int off, int len) throws IOException { } private int arrayContains(char[] array, int off, int len, char[] search) { - for (int i = off; i < len - search.length; i++) { + for (int i = off; i <= off + len - search.length; i++) { if (array[i] == search[0]) { boolean found = true; int k = i; From ac95148ee2e19b260fad5d11331e918ee3045fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chojnacki?= Date: Tue, 14 Jul 2026 21:46:16 +0200 Subject: [PATCH 3/5] Preserve boundary-spanning RUM injection matches --- .../buffer/InjectingPipeOutputStream.java | 23 +++++++++++++++++++ .../buffer/InjectingPipeWriter.java | 23 +++++++++++++++++++ .../InjectingPipeOutputStreamTest.groovy | 17 ++++++++++++++ .../buffer/InjectingPipeWriterTest.groovy | 17 ++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index ba117d659aa..548331dc89f 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -129,6 +129,16 @@ public void write(byte[] array, int off, int len) throws IOException { } if (len > bulkWriteThreshold) { + // A match that started in the previous write precedes every match wholly in this array. + if (matchingPos > 0 && arrayCompletesPendingMatch(array, off, len)) { + int pendingMatchLength = marker.length - matchingPos; + for (int i = off; i < off + pendingMatchLength; i++) { + write(array[i]); + } + write(array, off + pendingMatchLength, len - pendingMatchLength); + return; + } + // if the content is large enough, we can bulk write everything but the N trail and tail. // This because the buffer can already contain some byte from a previous single write. // Also we need to fill the buffer with the tail since we don't know about the next write. @@ -179,6 +189,19 @@ public void write(byte[] array, int off, int len) throws IOException { } } + private boolean arrayCompletesPendingMatch(byte[] array, int off, int len) { + int pendingMatchLength = marker.length - matchingPos; + if (pendingMatchLength > len) { + return false; + } + for (int i = 0; i < pendingMatchLength; i++) { + if (array[off + i] != marker[matchingPos + i]) { + return false; + } + } + return true; + } + private int arrayContains(byte[] array, int off, int len, byte[] search) { for (int i = off; i <= off + len - search.length; i++) { if (array[i] == search[0]) { diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index 00f2e8541f4..cef30f55341 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -129,6 +129,16 @@ public void write(char[] array, int off, int len) throws IOException { } if (len > bulkWriteThreshold) { + // A match that started in the previous write precedes every match wholly in this array. + if (matchingPos > 0 && arrayCompletesPendingMatch(array, off, len)) { + int pendingMatchLength = marker.length - matchingPos; + for (int i = off; i < off + pendingMatchLength; i++) { + write(array[i]); + } + write(array, off + pendingMatchLength, len - pendingMatchLength); + return; + } + // if the content is large enough, we can bulk write everything but the N trail and tail. // This because the buffer can already contain some byte from a previous single write. // Also we need to fill the buffer with the tail since we don't know about the next write. @@ -180,6 +190,19 @@ public void write(char[] array, int off, int len) throws IOException { } } + private boolean arrayCompletesPendingMatch(char[] array, int off, int len) { + int pendingMatchLength = marker.length - matchingPos; + if (pendingMatchLength > len) { + return false; + } + for (int i = 0; i < pendingMatchLength; i++) { + if (array[off + i] != marker[matchingPos + i]) { + return false; + } + } + return true; + } + private int arrayContains(char[] array, int off, int len, char[] search) { for (int i = off; i <= off + len - search.length; i++) { if (array[i] == search[0]) { diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy index 959b3ea6b9b..ed5525bbb4e 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStreamTest.groovy @@ -179,6 +179,23 @@ class InjectingPipeOutputStreamTest extends DDSpecification { "with a marker at the end" | "dynamic-content" | "dynamic-content" | 1 } + def 'should prioritize a marker spanning a bulk write boundary'() { + setup: + def prefix = "ignored-prefix" + def payload = ">0123456789" + def source = (prefix + payload + "ignored-suffix").getBytes("UTF-8") + def downstream = new ByteArrayOutputStream() + def piped = new InjectingPipeOutputStream(downstream, MARKER_BYTES, CONTEXT_BYTES) + + when: + piped.write("abc0123456789".getBytes("UTF-8") + } + def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: def testBytes = "test content".getBytes("UTF-8") diff --git a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy index 840742b6d5e..19307f61c2b 100644 --- a/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy +++ b/dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriterTest.groovy @@ -194,6 +194,23 @@ class InjectingPipeWriterTest extends DDSpecification { "with a marker at the end" | "dynamic-content" | "dynamic-content" | 1 } + def 'should prioritize a marker spanning a bulk write boundary'() { + setup: + def prefix = "ignored-prefix" + def payload = ">0123456789" + def source = (prefix + payload + "ignored-suffix").toCharArray() + def downstream = new StringWriter() + def piped = new InjectingPipeWriter(downstream, MARKER_CHARS, CONTEXT_CHARS) + + when: + piped.write("abc0123456789" + } + def 'should be resilient to exceptions when onBytesWritten callback is null'() { setup: def downstream = new StringWriter() From 78485a01b91998b55654ea48908ab761ea9eafb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chojnacki?= Date: Wed, 15 Jul 2026 17:01:24 +0200 Subject: [PATCH 4/5] Refine RUM injection offset writes --- .../buffer/InjectingPipeOutputStream.java | 19 ++--- .../buffer/InjectingPipeWriter.java | 19 ++--- ...ractSpringBootWebmvcIntegrationTest.groovy | 53 +++++++++++++ .../SpringBootWebmvcIntegrationTest.groovy | 78 +------------------ .../SpringBootWebmvcRumInjectionTest.groovy | 45 +++++++++++ 5 files changed, 120 insertions(+), 94 deletions(-) create mode 100644 dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy create mode 100644 dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java index 548331dc89f..9d49166bd1b 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeOutputStream.java @@ -118,6 +118,7 @@ public void write(int b) throws IOException { @Override public void write(byte[] array, int off, int len) throws IOException { + final int end = off + len; if (!filter) { if (wasDraining) { // needs drain @@ -130,7 +131,7 @@ public void write(byte[] array, int off, int len) throws IOException { if (len > bulkWriteThreshold) { // A match that started in the previous write precedes every match wholly in this array. - if (matchingPos > 0 && arrayCompletesPendingMatch(array, off, len)) { + if (matchingPos > 0 && arrayCompletesPendingMatch(array, off, end)) { int pendingMatchLength = marker.length - matchingPos; for (int i = off; i < off + pendingMatchLength; i++) { write(array[i]); @@ -142,7 +143,7 @@ public void write(byte[] array, int off, int len) throws IOException { // if the content is large enough, we can bulk write everything but the N trail and tail. // This because the buffer can already contain some byte from a previous single write. // Also we need to fill the buffer with the tail since we don't know about the next write. - int idx = arrayContains(array, off, len, marker); + int idx = arrayContains(array, off, end, marker); if (idx >= 0) { // we have a full match. just write everything filter = false; @@ -159,7 +160,7 @@ public void write(byte[] array, int off, int len) throws IOException { if (onContentInjected != null) { onContentInjected.run(); } - bytesToWrite = off + len - idx; + bytesToWrite = end - idx; downstream.write(array, idx, bytesToWrite); bytesWritten += bytesToWrite; } else { @@ -177,21 +178,21 @@ public void write(byte[] array, int off, int len) throws IOException { downstream.write(array, off + marker.length - 1, bytesToWrite); bytesWritten += bytesToWrite; filter = wasFiltering; - for (int i = off + len - marker.length + 1; i < off + len; i++) { + for (int i = end - marker.length + 1; i < end; i++) { write(array[i]); } } } else { // use slow path because the length to write is small and within the lookbehind buffer size - for (int i = off; i < off + len; i++) { + for (int i = off; i < end; i++) { write(array[i]); } } } - private boolean arrayCompletesPendingMatch(byte[] array, int off, int len) { + private boolean arrayCompletesPendingMatch(byte[] array, int off, int end) { int pendingMatchLength = marker.length - matchingPos; - if (pendingMatchLength > len) { + if (off + pendingMatchLength > end) { return false; } for (int i = 0; i < pendingMatchLength; i++) { @@ -202,8 +203,8 @@ private boolean arrayCompletesPendingMatch(byte[] array, int off, int len) { return true; } - private int arrayContains(byte[] array, int off, int len, byte[] search) { - for (int i = off; i <= off + len - search.length; i++) { + private int arrayContains(byte[] array, int off, int end, byte[] search) { + for (int i = off; i <= end - search.length; i++) { if (array[i] == search[0]) { boolean found = true; int k = i; diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java index cef30f55341..a7439abaacf 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/buffer/InjectingPipeWriter.java @@ -118,6 +118,7 @@ public void write(int c) throws IOException { @Override public void write(char[] array, int off, int len) throws IOException { + final int end = off + len; if (!filter) { if (wasDraining) { // needs drain @@ -130,7 +131,7 @@ public void write(char[] array, int off, int len) throws IOException { if (len > bulkWriteThreshold) { // A match that started in the previous write precedes every match wholly in this array. - if (matchingPos > 0 && arrayCompletesPendingMatch(array, off, len)) { + if (matchingPos > 0 && arrayCompletesPendingMatch(array, off, end)) { int pendingMatchLength = marker.length - matchingPos; for (int i = off; i < off + pendingMatchLength; i++) { write(array[i]); @@ -142,7 +143,7 @@ public void write(char[] array, int off, int len) throws IOException { // if the content is large enough, we can bulk write everything but the N trail and tail. // This because the buffer can already contain some byte from a previous single write. // Also we need to fill the buffer with the tail since we don't know about the next write. - int idx = arrayContains(array, off, len, marker); + int idx = arrayContains(array, off, end, marker); if (idx >= 0) { // we have a full match. just write everything filter = false; @@ -159,7 +160,7 @@ public void write(char[] array, int off, int len) throws IOException { if (onContentInjected != null) { onContentInjected.run(); } - bytesToWrite = off + len - idx; + bytesToWrite = end - idx; downstream.write(array, idx, bytesToWrite); bytesWritten += bytesToWrite; } else { @@ -178,21 +179,21 @@ public void write(char[] array, int off, int len) throws IOException { bytesWritten += bytesToWrite; filter = wasFiltering; - for (int i = off + len - marker.length + 1; i < off + len; i++) { + for (int i = end - marker.length + 1; i < end; i++) { write(array[i]); } } } else { // use slow path because the length to write is small and within the lookbehind buffer size - for (int i = off; i < off + len; i++) { + for (int i = off; i < end; i++) { write(array[i]); } } } - private boolean arrayCompletesPendingMatch(char[] array, int off, int len) { + private boolean arrayCompletesPendingMatch(char[] array, int off, int end) { int pendingMatchLength = marker.length - matchingPos; - if (pendingMatchLength > len) { + if (off + pendingMatchLength > end) { return false; } for (int i = 0; i < pendingMatchLength; i++) { @@ -203,8 +204,8 @@ private boolean arrayCompletesPendingMatch(char[] array, int off, int len) { return true; } - private int arrayContains(char[] array, int off, int len, char[] search) { - for (int i = off; i <= off + len - search.length; i++) { + private int arrayContains(char[] array, int off, int end, char[] search) { + for (int i = off; i <= end - search.length; i++) { if (array[i] == search[0]) { boolean found = true; int k = i; diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy new file mode 100644 index 00000000000..dfa098408e2 --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy @@ -0,0 +1,53 @@ +import datadog.smoketest.AbstractServerSmokeTest + +import java.util.concurrent.atomic.AtomicInteger +import java.util.regex.Pattern + +abstract class AbstractSpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { + + protected List additionalJavaProperties() { + [] + } + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.uberJar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll(additionalJavaProperties()) + command.addAll((String[]) [ + "-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter", + "-jar", + springBootShadowJar, + "--server.port=${httpPort}" + ]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + @Override + File createTemporaryFile() { + return File.createTempFile("trace-structure-docs", "out") + } + + @Override + protected Set assertTraceCounts(Set expected, Map traceCounts) { + List remaining = expected.collect { Pattern.compile(it) }.toList() + for (def i = remaining.size() - 1; i >= 0; i--) { + for (Map.Entry entry : traceCounts.entrySet()) { + if (entry.getValue() > 0 && remaining.get(i).matcher(entry.getKey()).matches()) { + remaining.remove(i) + break + } + } + } + return remaining.collect { it.pattern() }.toSet() + } + + @Override + List expectedTelemetryDependencies() { + ['spring-core'] + } +} diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy index c3e7c9b6f04..e6872405051 100644 --- a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy @@ -1,60 +1,15 @@ -import datadog.smoketest.AbstractServerSmokeTest import okhttp3.Request -import java.util.concurrent.atomic.AtomicInteger -import java.util.regex.Pattern - -class SpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { - - @Override - ProcessBuilder createProcessBuilder() { - String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.uberJar.path") - - List command = new ArrayList<>() - command.add(javaPath()) - command.addAll(defaultJavaProperties) - command.addAll((String[]) [ - "-Ddd.rum.enabled=true", - "-Ddd.rum.application.id=appid", - "-Ddd.rum.client.token=token", - "-Ddd.rum.remote.configuration.id=12345", - "-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter", - "-jar", - springBootShadowJar, - "--server.port=${httpPort}" - ]) - ProcessBuilder processBuilder = new ProcessBuilder(command) - processBuilder.directory(new File(buildDirectory)) - } - - @Override - File createTemporaryFile() { - return File.createTempFile("trace-structure-docs", "out") - } +class SpringBootWebmvcIntegrationTest extends AbstractSpringBootWebmvcIntegrationTest { @Override protected Set expectedTraces() { return [ "\\[servlet\\.request:GET /fruits\\[spring\\.handler:FruitController\\.listFruits\\[repository\\.operation:FruitRepository\\.findAll\\[h2\\.query:.*", - "\\[servlet\\.request:GET /fruits/\\{name}\\[spring\\.handler:FruitController\\.findOneFruit\\[repository\\.operation:FruitRepository\\.findByName\\[h2\\.query:.*", - "\\[servlet\\.request:GET /form-response\\[spring\\.handler:FormResponseController\\.formResponse.*" + "\\[servlet\\.request:GET /fruits/\\{name\\}\\[spring\\.handler:FruitController\\.findOneFruit\\[repository\\.operation:FruitRepository\\.findByName\\[h2\\.query:.*" ] } - @Override - protected Set assertTraceCounts(Set expected, Map traceCounts) { - List remaining = expected.collect { Pattern.compile(it) }.toList() - for (def i = remaining.size() - 1; i >= 0; i--) { - for (Map.Entry entry : traceCounts.entrySet()) { - if (entry.getValue() > 0 && remaining.get(i).matcher(entry.getKey()).matches()) { - remaining.remove(i) - break - } - } - } - return remaining.collect { it.pattern() }.toSet() - } - def "find all fruits"() { setup: String url = "http://localhost:${httpPort}/fruits" @@ -83,33 +38,4 @@ class SpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { responseBodyStr.contains("banana") waitForTraceCount(1) } - - def "inject RUM without corrupting a Thymeleaf form response"() { - setup: - String url = "http://localhost:${httpPort}/form-response" - - when: - def response = client.newCall(new Request.Builder().url(url).get().build()).execute() - - then: - response.code() == 200 - response.header("x-datadog-rum-injected") == "1" - def responseBodyStr = response.body().string() - responseBodyStr.contains('const returnUrl = "https:\\/\\/app.example.test\\/flows\\/complete?request=request-123";') - responseBodyStr.contains('window.formResponse = { returnUrl: returnUrl };') - responseBodyStr.contains('onload="document.getElementById(\'response-form\').submit()"') - responseBodyStr.contains('action="https://provider.example.test/flows/continue"') - responseBodyStr.contains('value="request-123"') - responseBodyStr.count("DD_RUM.init(") == 1 - responseBodyStr.contains("https://www.datadoghq-browser-agent.com") - responseBodyStr.count("") == 1 - responseBodyStr.indexOf("DD_RUM.init(") < responseBodyStr.indexOf("") - responseBodyStr.trim().endsWith("") - waitForTraceCount(1) - } - - @Override - List expectedTelemetryDependencies() { - ['spring-core'] - } } diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy new file mode 100644 index 00000000000..0cd31a7265e --- /dev/null +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy @@ -0,0 +1,45 @@ +import okhttp3.Request + +class SpringBootWebmvcRumInjectionTest extends AbstractSpringBootWebmvcIntegrationTest { + + @Override + protected List additionalJavaProperties() { + [ + "-Ddd.rum.enabled=true", + "-Ddd.rum.application.id=appid", + "-Ddd.rum.client.token=token", + "-Ddd.rum.remote.configuration.id=12345" + ] + } + + @Override + protected Set expectedTraces() { + [ + "\\[servlet\\.request:GET /form-response\\[spring\\.handler:FormResponseController\\.formResponse.*" + ] + } + + def "inject RUM without corrupting a Thymeleaf form response"() { + setup: + String url = "http://localhost:${httpPort}/form-response" + + when: + def response = client.newCall(new Request.Builder().url(url).get().build()).execute() + + then: + response.code() == 200 + response.header("x-datadog-rum-injected") == "1" + def responseBodyStr = response.body().string() + responseBodyStr.contains('const returnUrl = "https:\\/\\/app.example.test\\/flows\\/complete?request=request-123";') + responseBodyStr.contains('window.formResponse = { returnUrl: returnUrl };') + responseBodyStr.contains('onload="document.getElementById(\'response-form\').submit()"') + responseBodyStr.contains('action="https://provider.example.test/flows/continue"') + responseBodyStr.contains('value="request-123"') + responseBodyStr.count("DD_RUM.init(") == 1 + responseBodyStr.contains("https://www.datadoghq-browser-agent.com") + responseBodyStr.count("") == 1 + responseBodyStr.indexOf("DD_RUM.init(") < responseBodyStr.indexOf("") + responseBodyStr.trim().endsWith("") + waitForTraceCount(1) + } +} From e050c08c4d3521307f01058aa1650b1b888ab603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chojnacki?= Date: Wed, 15 Jul 2026 19:12:22 +0200 Subject: [PATCH 5/5] Consolidate Spring Boot WebMVC smoke tests --- ...ractSpringBootWebmvcIntegrationTest.groovy | 53 ---------- .../SpringBootWebmvcIntegrationTest.groovy | 97 +++++++++++++++++++ .../SpringBootWebmvcRumInjectionTest.groovy | 45 --------- 3 files changed, 97 insertions(+), 98 deletions(-) delete mode 100644 dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy delete mode 100644 dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy deleted file mode 100644 index dfa098408e2..00000000000 --- a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/AbstractSpringBootWebmvcIntegrationTest.groovy +++ /dev/null @@ -1,53 +0,0 @@ -import datadog.smoketest.AbstractServerSmokeTest - -import java.util.concurrent.atomic.AtomicInteger -import java.util.regex.Pattern - -abstract class AbstractSpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { - - protected List additionalJavaProperties() { - [] - } - - @Override - ProcessBuilder createProcessBuilder() { - String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.uberJar.path") - - List command = new ArrayList<>() - command.add(javaPath()) - command.addAll(defaultJavaProperties) - command.addAll(additionalJavaProperties()) - command.addAll((String[]) [ - "-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter", - "-jar", - springBootShadowJar, - "--server.port=${httpPort}" - ]) - ProcessBuilder processBuilder = new ProcessBuilder(command) - processBuilder.directory(new File(buildDirectory)) - } - - @Override - File createTemporaryFile() { - return File.createTempFile("trace-structure-docs", "out") - } - - @Override - protected Set assertTraceCounts(Set expected, Map traceCounts) { - List remaining = expected.collect { Pattern.compile(it) }.toList() - for (def i = remaining.size() - 1; i >= 0; i--) { - for (Map.Entry entry : traceCounts.entrySet()) { - if (entry.getValue() > 0 && remaining.get(i).matcher(entry.getKey()).matches()) { - remaining.remove(i) - break - } - } - } - return remaining.collect { it.pattern() }.toSet() - } - - @Override - List expectedTelemetryDependencies() { - ['spring-core'] - } -} diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy index e6872405051..42d8db55947 100644 --- a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy +++ b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcIntegrationTest.groovy @@ -1,5 +1,58 @@ +import datadog.smoketest.AbstractServerSmokeTest import okhttp3.Request +import java.util.concurrent.atomic.AtomicInteger +import java.util.regex.Pattern + +abstract class AbstractSpringBootWebmvcIntegrationTest extends AbstractServerSmokeTest { + + protected List additionalJavaProperties() { + [] + } + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty("datadog.smoketest.springboot.uberJar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.addAll(additionalJavaProperties()) + command.addAll((String[]) [ + "-Ddd.writer.type=MultiWriter:TraceStructureWriter:${output.getAbsolutePath()}:includeResource,DDAgentWriter", + "-jar", + springBootShadowJar, + "--server.port=${httpPort}" + ]) + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + @Override + File createTemporaryFile() { + return File.createTempFile("trace-structure-docs", "out") + } + + @Override + protected Set assertTraceCounts(Set expected, Map traceCounts) { + List remaining = expected.collect { Pattern.compile(it) }.toList() + for (def i = remaining.size() - 1; i >= 0; i--) { + for (Map.Entry entry : traceCounts.entrySet()) { + if (entry.getValue() > 0 && remaining.get(i).matcher(entry.getKey()).matches()) { + remaining.remove(i) + break + } + } + } + return remaining.collect { it.pattern() }.toSet() + } + + @Override + List expectedTelemetryDependencies() { + ['spring-core'] + } +} + class SpringBootWebmvcIntegrationTest extends AbstractSpringBootWebmvcIntegrationTest { @Override @@ -39,3 +92,47 @@ class SpringBootWebmvcIntegrationTest extends AbstractSpringBootWebmvcIntegratio waitForTraceCount(1) } } + +class SpringBootWebmvcRumInjectionTest extends AbstractSpringBootWebmvcIntegrationTest { + + @Override + protected List additionalJavaProperties() { + [ + "-Ddd.rum.enabled=true", + "-Ddd.rum.application.id=appid", + "-Ddd.rum.client.token=token", + "-Ddd.rum.remote.configuration.id=12345" + ] + } + + @Override + protected Set expectedTraces() { + [ + "\\[servlet\\.request:GET /form-response\\[spring\\.handler:FormResponseController\\.formResponse.*" + ] + } + + def "inject RUM without corrupting a Thymeleaf form response"() { + setup: + String url = "http://localhost:${httpPort}/form-response" + + when: + def response = client.newCall(new Request.Builder().url(url).get().build()).execute() + + then: + response.code() == 200 + response.header("x-datadog-rum-injected") == "1" + def responseBodyStr = response.body().string() + responseBodyStr.contains('const returnUrl = "https:\\/\\/app.example.test\\/flows\\/complete?request=request-123";') + responseBodyStr.contains('window.formResponse = { returnUrl: returnUrl };') + responseBodyStr.contains('onload="document.getElementById(\'response-form\').submit()"') + responseBodyStr.contains('action="https://provider.example.test/flows/continue"') + responseBodyStr.contains('value="request-123"') + responseBodyStr.count("DD_RUM.init(") == 1 + responseBodyStr.contains("https://www.datadoghq-browser-agent.com") + responseBodyStr.count("") == 1 + responseBodyStr.indexOf("DD_RUM.init(") < responseBodyStr.indexOf("") + responseBodyStr.trim().endsWith("") + waitForTraceCount(1) + } +} diff --git a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy b/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy deleted file mode 100644 index 0cd31a7265e..00000000000 --- a/dd-smoke-tests/spring-boot-3.3-webmvc/src/test/groovy/SpringBootWebmvcRumInjectionTest.groovy +++ /dev/null @@ -1,45 +0,0 @@ -import okhttp3.Request - -class SpringBootWebmvcRumInjectionTest extends AbstractSpringBootWebmvcIntegrationTest { - - @Override - protected List additionalJavaProperties() { - [ - "-Ddd.rum.enabled=true", - "-Ddd.rum.application.id=appid", - "-Ddd.rum.client.token=token", - "-Ddd.rum.remote.configuration.id=12345" - ] - } - - @Override - protected Set expectedTraces() { - [ - "\\[servlet\\.request:GET /form-response\\[spring\\.handler:FormResponseController\\.formResponse.*" - ] - } - - def "inject RUM without corrupting a Thymeleaf form response"() { - setup: - String url = "http://localhost:${httpPort}/form-response" - - when: - def response = client.newCall(new Request.Builder().url(url).get().build()).execute() - - then: - response.code() == 200 - response.header("x-datadog-rum-injected") == "1" - def responseBodyStr = response.body().string() - responseBodyStr.contains('const returnUrl = "https:\\/\\/app.example.test\\/flows\\/complete?request=request-123";') - responseBodyStr.contains('window.formResponse = { returnUrl: returnUrl };') - responseBodyStr.contains('onload="document.getElementById(\'response-form\').submit()"') - responseBodyStr.contains('action="https://provider.example.test/flows/continue"') - responseBodyStr.contains('value="request-123"') - responseBodyStr.count("DD_RUM.init(") == 1 - responseBodyStr.contains("https://www.datadoghq-browser-agent.com") - responseBodyStr.count("") == 1 - responseBodyStr.indexOf("DD_RUM.init(") < responseBodyStr.indexOf("") - responseBodyStr.trim().endsWith("") - waitForTraceCount(1) - } -}