expectedExtraErrorInformation(ServerEndpoint endpoint) {
if (endpoint.throwsException) {
["error.message": "${endpoint.body}",
- "error.type": { it == Exception.name || it == InputMismatchException.name },
- "error.stack": String]
+ "error.type" : { it == Exception.name || it == InputMismatchException.name },
+ "error.stack" : String]
} else {
Collections.emptyMap()
}
@@ -233,6 +235,77 @@ class JettyServlet3TestSync extends JettyServlet3Test {
}
}
+class JettyServlet3SyncRumInjectionForkedTest extends JettyServlet3TestSync {
+
+ static class RumServlet extends HttpServlet {
+ private final String mimeType
+
+ RumServlet(String mime) {
+ this.mimeType = mime
+ }
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType(mimeType)
+ try (def writer = resp.getWriter()) {
+ writer.println("\n" +
+ "\n" +
+ "\n" +
+ " \n" +
+ " This is the title of the webpage!\n" +
+ " \n" +
+ " \n" +
+ " This is an example paragraph. Anything in the body tag will appear on the page, just like this p tag and its contents.
\n" +
+ " \n" +
+ "")
+ }
+ }
+ }
+
+ static class HtmlRumServlet extends RumServlet {
+ HtmlRumServlet() {
+ super("text/html")
+ }
+ }
+
+ static class XmlRumServlet extends RumServlet {
+ XmlRumServlet() {
+ super("text/xml")
+ }
+ }
+
+ @Override
+ protected void configurePreAgent() {
+ super.configurePreAgent()
+ injectSysConfig("rum.enabled", "true")
+ injectSysConfig("rum.application.id", "test")
+ injectSysConfig("rum.client.token", "secret")
+ }
+
+ @Override
+ protected void setupServlets(ServletContextHandler servletContextHandler) {
+ super.setupServlets(servletContextHandler)
+ addServlet(servletContextHandler, "/gimme-html", HtmlRumServlet)
+ addServlet(servletContextHandler, "/gimme-xml", XmlRumServlet)
+ }
+
+ def "test rum injection in head for mime #mime"() {
+ setup:
+ def request = new okhttp3.Request.Builder().url(server.address().resolve("gimme-$mime").toURL())
+ .get().build()
+ when:
+ def response = client.newCall(request).execute()
+ then:
+ assert response.code() == 200
+ assert response.body().string().contains(new String(RumInjector.getSnippet("UTF-8"), "UTF-8")) == expected
+ assert response.header("x-datadog-rum-injected") == (expected ? "1" : null)
+ where:
+ mime | expected
+ "html" | true
+ "xml" | false
+ }
+}
+
class JettyServlet3SyncV1ForkedTest extends JettyServlet3TestSync implements TestingGenericHttpNamingConventions.ServerV1 {
@@ -260,6 +333,7 @@ class JettyServlet3TestAsync extends JettyServlet3Test {
class JettyServlet3ASyncV1ForkedTest extends JettyServlet3TestAsync implements TestingGenericHttpNamingConventions.ServerV1 {
}
+
class JettyServlet3TestFakeAsync extends JettyServlet3Test {
@Override
@@ -586,16 +660,16 @@ class IastJettyServlet3ForkedTest extends JettyServlet3TestSync {
client.newCall(request).execute()
then:
- 1 * appModule.onRealPath(_)
- 1 * appModule.checkSessionTrackingModes(_)
+ 1 * appModule.onRealPath(_)
+ 1 * appModule.checkSessionTrackingModes(_)
0 * _
when:
client.newCall(request).execute()
then: //Only call once per application context
- 0 * appModule.onRealPath(_)
- 0 * appModule.checkSessionTrackingModes(_)
+ 0 * appModule.onRealPath(_)
+ 0 * appModule.checkSessionTrackingModes(_)
0 * _
}
diff --git a/dd-smoke-tests/rum/build.gradle b/dd-smoke-tests/rum/build.gradle
new file mode 100644
index 00000000000..f5a54e7985a
--- /dev/null
+++ b/dd-smoke-tests/rum/build.gradle
@@ -0,0 +1,7 @@
+apply from: "$rootDir/gradle/java.gradle"
+
+description = 'appsec-smoke-tests'
+
+dependencies {
+ api project(':dd-smoke-tests')
+}
diff --git a/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy
new file mode 100644
index 00000000000..dade3311a41
--- /dev/null
+++ b/dd-smoke-tests/rum/src/main/groovy/datadog/smoketest/rum/AbstractRumServerSmokeTest.groovy
@@ -0,0 +1,21 @@
+package datadog.smoketest.rum
+
+import datadog.smoketest.AbstractServerSmokeTest
+import okhttp3.Response
+import spock.lang.Shared
+
+class AbstractRumServerSmokeTest extends AbstractServerSmokeTest {
+ @Shared
+ protected String[] defaultRumProperties = [
+ "-Ddd.rum.enabled=true",
+ "-Ddd.rum.application.id=appid",
+ "-Ddd.rum.client.token=token"
+ ]
+
+
+ static void assertRumInjected(Response response) {
+ assert response.header('x-datadog-rum-injected') == '1': 'RUM injected header missing'
+ def content = response.body().string()
+ assert content.contains('https://www.datadoghq-browser-agent.com'): 'RUM script not injected'
+ }
+}
diff --git a/dd-smoke-tests/rum/tomcat-9/build.gradle b/dd-smoke-tests/rum/tomcat-9/build.gradle
new file mode 100644
index 00000000000..2c4d856c541
--- /dev/null
+++ b/dd-smoke-tests/rum/tomcat-9/build.gradle
@@ -0,0 +1,26 @@
+plugins {
+ id 'com.gradleup.shadow'
+}
+
+apply from: "$rootDir/gradle/java.gradle"
+description = 'RUM Tomcat 9 Smoke Tests'
+
+dependencies {
+ implementation 'org.apache.tomcat.embed:tomcat-embed-core:9.0.88'
+ implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.88'
+ implementation 'javax.servlet:javax.servlet-api:4.0.1'
+
+ testImplementation project(':dd-smoke-tests:rum')
+}
+
+jar {
+ manifest {
+ attributes('Main-Class': 'com.example.Main')
+ }
+}
+
+tasks.withType(Test).configureEach {
+ dependsOn "shadowJar"
+
+ jvmArgs "-Ddatadog.smoketest.rum.tomcat9.shadowJar.path=${tasks.shadowJar.archiveFile.get()}"
+}
diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java
new file mode 100644
index 00000000000..5639cb65677
--- /dev/null
+++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/HelloServlet.java
@@ -0,0 +1,29 @@
+package com.example;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class HelloServlet extends HttpServlet {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.setContentType("text/html;charset=UTF-8");
+ try (final PrintWriter writer = resp.getWriter()) {
+ writer.write(
+ ""
+ + ""
+ + ""
+ + " "
+ + " "
+ + " Hello Servlet"
+ + ""
+ + ""
+ + " Hello from Tomcat 9 Servlet!
"
+ + " This is a demo HTML page served by Java servlet.
"
+ + ""
+ + "");
+ }
+ }
+}
diff --git a/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java
new file mode 100644
index 00000000000..a69a8701d89
--- /dev/null
+++ b/dd-smoke-tests/rum/tomcat-9/src/main/java/com/example/Main.java
@@ -0,0 +1,35 @@
+package com.example;
+
+import java.io.File;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.startup.Tomcat;
+
+public class Main {
+ public static void main(String[] args) throws LifecycleException {
+ int port = 8080;
+ if (args.length == 1) {
+ port = Integer.parseInt(args[0]);
+ }
+
+ Tomcat tomcat = new Tomcat();
+ tomcat.setPort(port);
+ tomcat.getConnector(); // This is required to make Tomcat start
+ tomcat.setBaseDir(".");
+
+ // Add webapp context
+ String contextPath = "";
+ String docBase = new File(".").getAbsolutePath();
+ Context context = tomcat.addContext(contextPath, docBase);
+
+ // Add servlet programmatically
+ context.addServletContainerInitializer(
+ (c, ctx) -> {
+ ctx.addServlet("helloServlet", new HelloServlet()).addMapping("/hello");
+ },
+ null);
+
+ tomcat.start();
+ tomcat.getServer().await();
+ }
+}
diff --git a/dd-smoke-tests/rum/tomcat-9/src/test/groovy/datadog/smoketest/rum/tomcat9/Tomcat9RumSmokeTest.groovy b/dd-smoke-tests/rum/tomcat-9/src/test/groovy/datadog/smoketest/rum/tomcat9/Tomcat9RumSmokeTest.groovy
new file mode 100644
index 00000000000..25bfe66de87
--- /dev/null
+++ b/dd-smoke-tests/rum/tomcat-9/src/test/groovy/datadog/smoketest/rum/tomcat9/Tomcat9RumSmokeTest.groovy
@@ -0,0 +1,43 @@
+package datadog.smoketest.rum.tomcat9
+
+import datadog.smoketest.rum.AbstractRumServerSmokeTest
+import datadog.trace.api.Platform
+import okhttp3.Request
+import okhttp3.Response
+
+class Tomcat9RumSmokeTest extends AbstractRumServerSmokeTest {
+
+
+ @Override
+ ProcessBuilder createProcessBuilder() {
+ String jarPath = System.getProperty('datadog.smoketest.rum.tomcat9.shadowJar.path')
+
+ List command = []
+ command.add(javaPath())
+ command.addAll(defaultJavaProperties)
+ command.addAll(defaultRumProperties)
+ if (Platform.isJavaVersionAtLeast(17)) {
+ command.addAll((String[]) ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'])
+ }
+ command.addAll(['-jar', jarPath, Integer.toString(httpPort)])
+ ProcessBuilder processBuilder = new ProcessBuilder(command)
+ processBuilder.directory(new File(buildDirectory))
+ return processBuilder
+ }
+
+ void 'test RUM SDK injection'() {
+ given:
+ def url = "http://localhost:${httpPort}/hello"
+ def request = new Request.Builder()
+ .url(url)
+ .get()
+ .build()
+
+ when:
+ Response response = client.newCall(request).execute()
+
+ then:
+ response.code() == 200
+ assertRumInjected(response)
+ }
+}
diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java
index a6d5888e76f..c3656cd3251 100644
--- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java
+++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java
@@ -240,6 +240,11 @@ public final class ConfigDefaults {
static final boolean DEFAULT_TELEMETRY_LOG_COLLECTION_ENABLED = true;
static final int DEFAULT_TELEMETRY_DEPENDENCY_RESOLUTION_QUEUE_SIZE = 100000;
+ static final boolean DEFAULT_RUM_ENABLED = false;
+ static final int DEFAULT_RUM_MAJOR_VERSION = 6;
+ static final float DEFAULT_RUM_SESSION_SAMPLE_RATE = 100f;
+ static final float DEFAULT_RUM_SESSION_REPLAY_SAMPLE_RATE = 100f;
+
static final boolean DEFAULT_SSI_INJECTION_FORCE = false;
static final String DEFAULT_INSTRUMENTATION_SOURCE = "manual";
diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/RumConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/RumConfig.java
new file mode 100644
index 00000000000..51d90fd31c4
--- /dev/null
+++ b/dd-trace-api/src/main/java/datadog/trace/api/config/RumConfig.java
@@ -0,0 +1,20 @@
+package datadog.trace.api.config;
+
+public final class RumConfig {
+ public static final String RUM_ENABLED = "rum.enabled";
+ public static final String RUM_APPLICATION_ID = "rum.application.id";
+ public static final String RUM_CLIENT_TOKEN = "rum.client.token";
+ public static final String RUM_SITE = "rum.site";
+ public static final String RUM_SERVICE = "rum.service";
+ public static final String RUM_ENVIRONMENT = "rum.environment";
+ public static final String RUM_MAJOR_VERSION = "rum.major.version";
+ public static final String RUM_VERSION = "rum.version";
+ public static final String RUM_TRACK_USER_INTERACTION = "rum.track.user.interaction";
+ public static final String RUM_TRACK_RESOURCES = "rum.track.resources";
+ public static final String RUM_TRACK_LONG_TASKS = "rum.track.long.tasks";
+ public static final String RUM_DEFAULT_PRIVACY_LEVEL = "rum.default.privacy.level";
+ public static final String RUM_SESSION_SAMPLE_RATE = "rum.session.sample.rate";
+ public static final String RUM_SESSION_REPLAY_SAMPLE_RATE = "rum.session.replay.sample.rate";
+
+ private RumConfig() {}
+}
diff --git a/internal-api/build.gradle b/internal-api/build.gradle
index fbf1916a4af..424873f75be 100644
--- a/internal-api/build.gradle
+++ b/internal-api/build.gradle
@@ -244,6 +244,7 @@ dependencies {
api project(':dd-trace-api')
api libs.slf4j
api project(':components:context')
+ api project(':components:json')
api project(':components:yaml')
api project(':components:cli')
api project(":utils:time-utils")
diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java
index 6534d8f870c..56a55be4204 100644
--- a/internal-api/src/main/java/datadog/trace/api/Config.java
+++ b/internal-api/src/main/java/datadog/trace/api/Config.java
@@ -118,6 +118,10 @@
import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_POLL_INTERVAL_SECONDS;
import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_TARGETS_KEY;
import static datadog.trace.api.ConfigDefaults.DEFAULT_REMOTE_CONFIG_TARGETS_KEY_ID;
+import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_ENABLED;
+import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_MAJOR_VERSION;
+import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_SESSION_REPLAY_SAMPLE_RATE;
+import static datadog.trace.api.ConfigDefaults.DEFAULT_RUM_SESSION_SAMPLE_RATE;
import static datadog.trace.api.ConfigDefaults.DEFAULT_SCOPE_DEPTH_LIMIT;
import static datadog.trace.api.ConfigDefaults.DEFAULT_SCOPE_ITERATION_KEEP_ALIVE;
import static datadog.trace.api.ConfigDefaults.DEFAULT_SECURE_RANDOM;
@@ -461,6 +465,20 @@
import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_TARGETS_KEY;
import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_TARGETS_KEY_ID;
import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIG_URL;
+import static datadog.trace.api.config.RumConfig.RUM_APPLICATION_ID;
+import static datadog.trace.api.config.RumConfig.RUM_CLIENT_TOKEN;
+import static datadog.trace.api.config.RumConfig.RUM_DEFAULT_PRIVACY_LEVEL;
+import static datadog.trace.api.config.RumConfig.RUM_ENABLED;
+import static datadog.trace.api.config.RumConfig.RUM_ENVIRONMENT;
+import static datadog.trace.api.config.RumConfig.RUM_MAJOR_VERSION;
+import static datadog.trace.api.config.RumConfig.RUM_SERVICE;
+import static datadog.trace.api.config.RumConfig.RUM_SESSION_REPLAY_SAMPLE_RATE;
+import static datadog.trace.api.config.RumConfig.RUM_SESSION_SAMPLE_RATE;
+import static datadog.trace.api.config.RumConfig.RUM_SITE;
+import static datadog.trace.api.config.RumConfig.RUM_TRACK_LONG_TASKS;
+import static datadog.trace.api.config.RumConfig.RUM_TRACK_RESOURCES;
+import static datadog.trace.api.config.RumConfig.RUM_TRACK_USER_INTERACTION;
+import static datadog.trace.api.config.RumConfig.RUM_VERSION;
import static datadog.trace.api.config.TraceInstrumentationConfig.ADD_SPAN_POINTERS;
import static datadog.trace.api.config.TraceInstrumentationConfig.AXIS_PROMOTE_RESOURCE_NAME;
import static datadog.trace.api.config.TraceInstrumentationConfig.CASSANDRA_KEYSPACE_STATEMENT_EXTRACTION_ENABLED;
@@ -618,6 +636,8 @@
import datadog.trace.api.iast.telemetry.Verbosity;
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.api.profiling.ProfilingEnablement;
+import datadog.trace.api.rum.RumInjectorConfig;
+import datadog.trace.api.rum.RumInjectorConfig.PrivacyLevel;
import datadog.trace.bootstrap.config.provider.CapturedEnvironmentConfigSource;
import datadog.trace.bootstrap.config.provider.ConfigProvider;
import datadog.trace.bootstrap.config.provider.SystemPropertiesConfigSource;
@@ -1190,6 +1210,9 @@ public static String getHostName() {
private final int stackTraceLengthLimit;
+ private final boolean rumEnabled;
+ private final RumInjectorConfig rumInjectorConfig;
+
// Read order: System Properties -> Env Variables, [-> properties file], [-> default value]
private Config() {
this(ConfigProvider.createDefault());
@@ -2673,9 +2696,40 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
this.stackTraceLengthLimit =
configProvider.getInteger(STACK_TRACE_LENGTH_LIMIT, defaultStackTraceLengthLimit);
+ this.rumEnabled = configProvider.getBoolean(RUM_ENABLED, DEFAULT_RUM_ENABLED);
+ this.rumInjectorConfig = parseRumConfig(configProvider);
+
log.debug("New instance: {}", this);
}
+ private static RumInjectorConfig parseRumConfig(ConfigProvider configProvider) {
+ String applicationId = configProvider.getString(RUM_APPLICATION_ID);
+ String clientToken = configProvider.getString(RUM_CLIENT_TOKEN);
+ if (applicationId == null || clientToken == null) {
+ return null;
+ }
+ try {
+ return new RumInjectorConfig(
+ applicationId,
+ clientToken,
+ configProvider.getString(RUM_SITE),
+ configProvider.getString(RUM_SERVICE),
+ configProvider.getString(RUM_ENVIRONMENT),
+ configProvider.getInteger(RUM_MAJOR_VERSION, DEFAULT_RUM_MAJOR_VERSION),
+ configProvider.getString(RUM_VERSION),
+ configProvider.getBoolean(RUM_TRACK_USER_INTERACTION),
+ configProvider.getBoolean(RUM_TRACK_RESOURCES),
+ configProvider.getBoolean(RUM_TRACK_LONG_TASKS),
+ configProvider.getEnum(RUM_DEFAULT_PRIVACY_LEVEL, PrivacyLevel.class, null),
+ configProvider.getFloat(RUM_SESSION_SAMPLE_RATE, DEFAULT_RUM_SESSION_SAMPLE_RATE),
+ configProvider.getFloat(
+ RUM_SESSION_REPLAY_SAMPLE_RATE, DEFAULT_RUM_SESSION_REPLAY_SAMPLE_RATE));
+ } catch (IllegalArgumentException e) {
+ log.warn("Unable to configure RUM injection", e);
+ return null;
+ }
+ }
+
/**
* Converts a list of packages in Jacoco exclusion format ({@code
* my.package.*,my.other.package.*}) to list of package prefixes suitable for use with ASM ({@code
@@ -4908,6 +4962,14 @@ public int getCloudPayloadTaggingMaxTags() {
return cloudPayloadTaggingMaxTags;
}
+ public boolean isRumEnabled() {
+ return this.rumEnabled;
+ }
+
+ public RumInjectorConfig getRumInjectorConfig() {
+ return this.rumInjectorConfig;
+ }
+
private Set getSettingsSetFromEnvironment(
String name, Function mapper, boolean splitOnWS) {
final String value = configProvider.getString(name, "");
@@ -5588,6 +5650,10 @@ public String toString() {
+ cloudResponsePayloadTagging
+ ", experimentalPropagateProcessTagsEnabled="
+ experimentalPropagateProcessTagsEnabled
+ + ", rumEnabled="
+ + rumEnabled
+ + ", rumInjectorConfig="
+ + (rumInjectorConfig == null ? "null" : rumInjectorConfig.jsonPayload())
+ '}';
}
}
diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java
new file mode 100644
index 00000000000..25e337f67ec
--- /dev/null
+++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjector.java
@@ -0,0 +1,73 @@
+package datadog.trace.api.rum;
+
+import datadog.trace.api.Config;
+import datadog.trace.api.cache.DDCache;
+import datadog.trace.api.cache.DDCaches;
+import java.util.function.Function;
+
+public final class RumInjector {
+ private static volatile boolean initialized = false;
+ private static volatile boolean enabled;
+ private static volatile String snippet;
+
+ private static final String MARKER = "";
+ private static final DDCache SNIPPET_CACHE = DDCaches.newFixedSizeCache(16);
+ private static final DDCache MARKER_CACHE = DDCaches.newFixedSizeCache(16);
+ private static final Function SNIPPET_ADDER =
+ charset -> {
+ try {
+ return snippet.getBytes(charset);
+ } catch (Throwable t) {
+ return null;
+ }
+ };
+ private static final Function MARKER_ADDER =
+ charset -> {
+ try {
+ return MARKER.getBytes(charset);
+ } catch (Throwable t) {
+ return null;
+ }
+ };
+
+ /**
+ * Check whether RUM injection is enabled and ready to inject.
+ *
+ * @return {@code true} if enabled, {@code otherwise}.
+ */
+ public static boolean isEnabled() {
+ if (!initialized) {
+ Config config = Config.get();
+ boolean rumEnabled = config.isRumEnabled();
+ RumInjectorConfig injectorConfig = config.getRumInjectorConfig();
+ if (rumEnabled && injectorConfig != null) {
+ enabled = true;
+ snippet = injectorConfig.getSnippet();
+ } else {
+ enabled = false;
+ snippet = null;
+ }
+ initialized = true;
+ }
+ return enabled;
+ }
+
+ /**
+ * Get the HTML snippet to inject RUM SDK
+ *
+ * @return The HTML snippet to inject, {@code null} if RUM injection is disabled to inject.
+ */
+ public static byte[] getSnippet(String encoding) {
+ if (!isEnabled()) {
+ return null;
+ }
+ return SNIPPET_CACHE.computeIfAbsent(encoding, SNIPPET_ADDER);
+ }
+
+ public static byte[] getMarker(String encoding) {
+ if (!isEnabled()) {
+ return null;
+ }
+ return MARKER_CACHE.computeIfAbsent(encoding, MARKER_ADDER);
+ }
+}
diff --git a/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorConfig.java b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorConfig.java
new file mode 100644
index 00000000000..66b350c2b46
--- /dev/null
+++ b/internal-api/src/main/java/datadog/trace/api/rum/RumInjectorConfig.java
@@ -0,0 +1,187 @@
+package datadog.trace.api.rum;
+
+import static java.util.Locale.ROOT;
+
+import datadog.json.JsonWriter;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+public class RumInjectorConfig {
+ private static final String DEFAULT_SITE = "datadoghq.com";
+ private static final String GOV_CLOUD_SITE = "ddog-gov.com";
+ private static final Map REGIONS = new HashMap<>();
+
+ static {
+ REGIONS.put("datadoghq.com", "us1");
+ REGIONS.put("us3.datadoghq.com", "us3");
+ REGIONS.put("us5.datadoghq.com", "us5");
+ REGIONS.put("datadoghq.eu", "eu1");
+ REGIONS.put("ap1.datadoghq.com", "ap1");
+ }
+
+ /** RUM application ID */
+ public final String applicationId;
+ /** The client token provided by Datadog to authenticate requests. */
+ public final String clientToken;
+ /** The Datadog site to which data will be sent (e.g., `datadoghq.com`). */
+ public final String site;
+ /** The name of the service being monitored. */
+ @Nullable public final String service;
+ /** The environment of the service (e.g., `prod`, `staging` or `dev). */
+ @Nullable public final String env;
+ /** SDK major version. */
+ public int majorVersion;
+ /** The version of the service (e.g., `0.1.0`, `a8dj92`, `2024-30`). */
+ @Nullable public final String version;
+ /** Enables or disables the automatic collection of users actions (e.g., clicks). */
+ @Nullable public final Boolean trackUserInteractions;
+ /** Enables or disables the collection of resource events (e.g., loading of images or scripts). */
+ @Nullable public final Boolean trackResources;
+ /** Enables or disables the collection of long task events. */
+ @Nullable public final Boolean trackLongTask;
+ /** The privacy level for data collection. */
+ @Nullable public final PrivacyLevel defaultPrivacyLevel;
+ /** The percentage of user sessions to be tracked (between 0.0 and 100.0). */
+ public final float sessionSampleRate;
+ /**
+ * The percentage of tracked sessions that will include Session Replay data (between 0.0 and
+ * 100.0).
+ */
+ public final float sessionReplaySampleRate;
+
+ public RumInjectorConfig(
+ String applicationId,
+ String clientToken,
+ @Nullable String site,
+ @Nullable String service,
+ @Nullable String env,
+ int majorVersion,
+ @Nullable String version,
+ @Nullable Boolean trackUserInteractions,
+ @Nullable Boolean trackResources,
+ @Nullable Boolean trackLongTask,
+ @Nullable PrivacyLevel defaultPrivacyLevel,
+ float sessionSampleRate,
+ float sessionReplaySampleRate) {
+ if (applicationId == null || applicationId.isEmpty()) {
+ throw new IllegalArgumentException("Invalid application id: " + applicationId);
+ }
+ this.applicationId = applicationId;
+ if (clientToken == null || clientToken.isEmpty()) {
+ throw new IllegalArgumentException("Invalid client token: " + clientToken);
+ }
+ this.clientToken = clientToken;
+ if (site == null || site.isEmpty()) {
+ this.site = DEFAULT_SITE;
+ } else if (validateSite(site)) {
+ this.site = site;
+ } else {
+ throw new IllegalArgumentException("Invalid site: " + site);
+ }
+ this.service = service;
+ this.env = env;
+ if (majorVersion != 5 && majorVersion != 6) {
+ throw new IllegalArgumentException("Invalid major version: " + majorVersion);
+ }
+ this.majorVersion = majorVersion;
+ this.version = version;
+ this.trackUserInteractions = trackUserInteractions;
+ this.trackResources = trackResources;
+ this.trackLongTask = trackLongTask;
+ if (sessionSampleRate < 0f || sessionSampleRate > 100f) {
+ throw new IllegalArgumentException("Invalid session sample rate: " + sessionSampleRate);
+ }
+ this.sessionSampleRate = sessionSampleRate;
+ if (sessionReplaySampleRate < 0f || sessionReplaySampleRate > 100f) {
+ throw new IllegalArgumentException(
+ "Invalid session replay sample rate: " + sessionReplaySampleRate);
+ }
+ this.sessionReplaySampleRate = sessionReplaySampleRate;
+ this.defaultPrivacyLevel = defaultPrivacyLevel;
+ }
+
+ private static boolean validateSite(String site) {
+ for (String key : REGIONS.keySet()) {
+ if (key.equals(site)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String getSnippet() {
+ return "\n";
+ }
+
+ private String getCdnUrl() {
+ if (!GOV_CLOUD_SITE.equals(this.site)) {
+ return "https://www.datadoghq-browser-agent.com/datadog-rum-v" + this.majorVersion + ".js";
+ }
+ return "https://www.datadoghq-browser-agent.com/"
+ + REGIONS.get(this.site)
+ + "/v"
+ + this.majorVersion
+ + "/datadog-rum.js";
+ }
+
+ public String jsonPayload() {
+ try (JsonWriter writer = new JsonWriter()) {
+ writer.beginObject();
+ writer.name("application_id").value(this.applicationId);
+ writer.name("client_token").value(this.clientToken);
+ if (this.site != null) {
+ writer.name("site").value(this.site);
+ }
+ if (this.service != null) {
+ writer.name("service").value(this.service);
+ }
+ if (this.env != null) {
+ writer.name("env").value(this.env);
+ }
+ if (this.version != null) {
+ writer.name("version").value(this.version);
+ }
+ if (this.trackUserInteractions != null) {
+ writer.name("track_user_interactions").value(this.trackUserInteractions);
+ }
+ if (this.trackResources != null) {
+ writer.name("track_resources").value(this.trackResources);
+ }
+ if (this.trackLongTask != null) {
+ writer.name("track_long_task").value(this.trackLongTask);
+ }
+ if (this.defaultPrivacyLevel != null) {
+ writer.name("default_privacy_level").value(this.defaultPrivacyLevel.toJson());
+ }
+ writer.name("session_sample_rate").value(this.sessionSampleRate);
+ writer.name("session_replay_sample_rate").value(this.sessionReplaySampleRate);
+ return writer.toString();
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to generate config payload", e);
+ }
+ }
+
+ public enum PrivacyLevel {
+ ALLOW,
+ MASK,
+ MASK_USER_INPUT;
+
+ public String toJson() {
+ return toString().toLowerCase(ROOT);
+ }
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index cc17a3de79f..c414abaf11e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -153,6 +153,8 @@ include ':dd-smoke-tests:quarkus-native'
include ':dd-smoke-tests:sample-trace'
include ':dd-smoke-tests:ratpack-1.5'
include ':dd-smoke-tests:resteasy'
+include ':dd-smoke-tests:rum'
+include ':dd-smoke-tests:rum:tomcat-9'
include ':dd-smoke-tests:spring-boot-3.0-native'
include ':dd-smoke-tests:spring-boot-2.4-webflux'
include ':dd-smoke-tests:spring-boot-2.5-webflux'