From 5e64e973fe73881b178aed61781c98dfb7a4bcc0 Mon Sep 17 00:00:00 2001 From: Phil Hetzel Date: Sun, 13 Jul 2025 11:42:14 -0400 Subject: [PATCH] display Experiment URL to stdout automatically instead of specifying it manually --- .../SimpleExperimentWithRegistration.java | 24 +--- .../braintrust/examples/TriviaEvaluation.java | 19 +-- .../java/dev/braintrust/api/Experiment.java | 68 ++++++++-- .../dev/braintrust/api/ExperimentTest.java | 116 ++++++++++++++++++ 4 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 src/test/java/dev/braintrust/api/ExperimentTest.java diff --git a/examples/src/main/java/dev/braintrust/examples/SimpleExperimentWithRegistration.java b/examples/src/main/java/dev/braintrust/examples/SimpleExperimentWithRegistration.java index beb3679..aa2c219 100644 --- a/examples/src/main/java/dev/braintrust/examples/SimpleExperimentWithRegistration.java +++ b/examples/src/main/java/dev/braintrust/examples/SimpleExperimentWithRegistration.java @@ -38,28 +38,6 @@ public static void main(String[] args) throws Exception { var experiment = Experiment.registerExperimentSync(experimentName, project.id()); System.out.println("Experiment ID: " + experiment.id()); - // Get the app URL from config - var appUrl = config.appUrl().toString().replaceAll("/$", ""); - // URLEncoder.encode uses + for spaces, but we need %20 for URL paths - var projectNameEncoded = - java.net.URLEncoder.encode(project.name(), java.nio.charset.StandardCharsets.UTF_8) - .replace("+", "%20"); - - // For staging, we need to handle the org differently - var baseExperimentUrl = - appUrl.contains("staging") - ? String.format( - "%s/app/%s/p/%s/experiments/%s", - appUrl, project.orgId(), projectNameEncoded, experimentName) - : String.format( - "%s/app/braintrustdata.com/p/%s/experiments/%s", - appUrl, projectNameEncoded, experimentName); - - // TODO: Add comparison parameter if there's a previous experiment - var experimentUrl = baseExperimentUrl; - - System.out.println("\nExperiment " + experimentName + " is running at " + experimentUrl); - // Step 3: Set up tracing context with experiment (similar to Go's trace.SetParent) var braintrustContext = BraintrustContext.forExperiment(experiment.id()); var context = braintrustContext.storeInContext(Context.current()).makeCurrent(); @@ -138,7 +116,7 @@ public static void main(String[] args) throws Exception { } System.out.println("\n=== View in Braintrust ==="); - System.out.println("See results for " + experimentName + " at " + experimentUrl); + System.out.println("See results for " + experimentName + " in the Braintrust web app."); // Clean up context context.close(); diff --git a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java index f6a3da7..9ff82cd 100644 --- a/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java +++ b/examples/src/main/java/dev/braintrust/examples/TriviaEvaluation.java @@ -59,23 +59,6 @@ public static void main(String[] args) throws Exception { var experiment = Experiment.registerExperimentSync(experimentName, project.id()); System.out.println("Experiment ID: " + experiment.id()); - // Show experiment URL - var appUrl = config.appUrl().toString().replaceAll("/$", ""); - var orgNameEncoded = - java.net.URLEncoder.encode( - config.orgName().orElse("unknown"), - java.nio.charset.StandardCharsets.UTF_8) - .replace("+", "%20"); - var projectNameEncoded = - java.net.URLEncoder.encode(project.name(), java.nio.charset.StandardCharsets.UTF_8) - .replace("+", "%20"); - var experimentUrl = - String.format( - "%s/app/%s/p/%s/experiments/%s", - appUrl, orgNameEncoded, projectNameEncoded, experimentName); - - System.out.println("\nExperiment " + experimentName + " is running at " + experimentUrl); - // Step 3: Set up tracing context var braintrustContext = BraintrustContext.forExperiment(experiment.id()); var context = braintrustContext.storeInContext(Context.current()).makeCurrent(); @@ -181,7 +164,7 @@ public static void main(String[] args) throws Exception { } System.out.println("\n=== View in Braintrust ==="); - System.out.println("See results for " + experimentName + " at " + experimentUrl); + System.out.println("See results for " + experimentName + " in the Braintrust web app."); // Clean up context.close(); diff --git a/src/main/java/dev/braintrust/api/Experiment.java b/src/main/java/dev/braintrust/api/Experiment.java index 3a39d53..6ae2a0b 100644 --- a/src/main/java/dev/braintrust/api/Experiment.java +++ b/src/main/java/dev/braintrust/api/Experiment.java @@ -1,6 +1,8 @@ package dev.braintrust.api; import dev.braintrust.config.BraintrustConfig; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -23,13 +25,7 @@ private Experiment() { public static CompletableFuture registerExperiment( String name, String projectId) { var config = BraintrustConfig.fromEnvironment(); - var client = new BraintrustApiClient(config); - - var request = - new BraintrustApiClient.CreateExperimentRequest( - projectId, name, Optional.empty(), Optional.empty()); - - return client.createExperiment(request); + return registerExperiment(name, projectId, config); } /** @@ -48,7 +44,22 @@ public static CompletableFuture registerExperime new BraintrustApiClient.CreateExperimentRequest( projectId, name, Optional.empty(), Optional.empty()); - return client.createExperiment(request); + return client.createExperiment(request) + .thenCompose( + experiment -> { + // Get project info to build URL + return client.getProject(projectId) + .thenApply( + projectOpt -> { + projectOpt.ifPresent( + project -> + displayExperimentUrl( + experiment, + project, + config)); + return experiment; + }); + }); } /** @@ -88,7 +99,12 @@ public static CompletableFuture getOrCreateExperiment( experimentName, Optional.empty(), Optional.empty()); - return client.createExperiment(request); + return client.createExperiment(request) + .thenApply( + experiment -> { + displayExperimentUrl(experiment, project, config); + return experiment; + }); }) .thenApply(experiment -> experiment.id()); } @@ -117,4 +133,38 @@ public static String getOrCreateExperimentSync(String experimentName, String pro throw new RuntimeException("Failed to get or create experiment", e); } } + + /** + * Displays the experiment URL to stdout automatically when an experiment is created. + * + * @param experiment The experiment that was created + * @param project The project containing the experiment + * @param config The Braintrust configuration + */ + private static void displayExperimentUrl( + BraintrustApiClient.Experiment experiment, + BraintrustApiClient.Project project, + BraintrustConfig config) { + try { + var appUrl = config.appUrl().toString().replaceAll("/$", ""); + var orgNameEncoded = + URLEncoder.encode(config.orgName().orElse("unknown"), StandardCharsets.UTF_8) + .replace("+", "%20"); + var projectNameEncoded = + URLEncoder.encode(project.name(), StandardCharsets.UTF_8).replace("+", "%20"); + var experimentNameEncoded = + URLEncoder.encode(experiment.name(), StandardCharsets.UTF_8) + .replace("+", "%20"); + var experimentUrl = + String.format( + "%s/app/%s/p/%s/experiments/%s", + appUrl, orgNameEncoded, projectNameEncoded, experimentNameEncoded); + + System.out.println( + "Experiment " + experiment.name() + " is running at " + experimentUrl); + } catch (Exception e) { + // Don't fail experiment creation if URL display fails + System.err.println("Warning: Could not display experiment URL: " + e.getMessage()); + } + } } diff --git a/src/test/java/dev/braintrust/api/ExperimentTest.java b/src/test/java/dev/braintrust/api/ExperimentTest.java new file mode 100644 index 0000000..47c2f4c --- /dev/null +++ b/src/test/java/dev/braintrust/api/ExperimentTest.java @@ -0,0 +1,116 @@ +package dev.braintrust.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.braintrust.config.BraintrustConfig; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.URI; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ExperimentTest { + + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outputStream)); + } + + @AfterEach + void tearDown() { + System.setOut(originalOut); + } + + @Test + void testDisplayExperimentUrl() throws Exception { + // Create test data + var experiment = + new BraintrustApiClient.Experiment( + "exp-123", + "proj-456", + "test-experiment", + Optional.of("Test description"), + "2024-01-01T00:00:00Z", + "2024-01-01T00:00:00Z"); + var project = + new BraintrustApiClient.Project( + "proj-456", + "Test Project", + "org-789", + "2024-01-01T00:00:00Z", + "2024-01-01T00:00:00Z"); + var config = + BraintrustConfig.builder() + .apiKey("test-key") + .apiUrl("https://api.braintrust.dev") + .appUrl(URI.create("https://www.braintrust.dev")) + .orgName("Test Org") + .build(); + + // Use reflection to call the private method + var method = + Experiment.class.getDeclaredMethod( + "displayExperimentUrl", + BraintrustApiClient.Experiment.class, + BraintrustApiClient.Project.class, + BraintrustConfig.class); + method.setAccessible(true); + method.invoke(null, experiment, project, config); + + // Verify the output + var output = outputStream.toString(); + assertThat(output).contains("Experiment test-experiment is running at"); + assertThat(output) + .contains( + "https://www.braintrust.dev/app/Test%20Org/p/Test%20Project/experiments/test-experiment"); + } + + @Test + void testDisplayExperimentUrlWithSpaces() throws Exception { + // Test with spaces in org and project names + var experiment = + new BraintrustApiClient.Experiment( + "exp-123", + "proj-456", + "test experiment", + Optional.of("Test description"), + "2024-01-01T00:00:00Z", + "2024-01-01T00:00:00Z"); + var project = + new BraintrustApiClient.Project( + "proj-456", + "Test Project With Spaces", + "org-789", + "2024-01-01T00:00:00Z", + "2024-01-01T00:00:00Z"); + var config = + BraintrustConfig.builder() + .apiKey("test-key") + .apiUrl("https://api.braintrust.dev") + .appUrl(URI.create("https://www.braintrust.dev")) + .orgName("Test Org With Spaces") + .build(); + + // Use reflection to call the private method + var method = + Experiment.class.getDeclaredMethod( + "displayExperimentUrl", + BraintrustApiClient.Experiment.class, + BraintrustApiClient.Project.class, + BraintrustConfig.class); + method.setAccessible(true); + method.invoke(null, experiment, project, config); + + // Verify the output with proper URL encoding + var output = outputStream.toString(); + assertThat(output).contains("Experiment test experiment is running at"); + assertThat(output) + .contains( + "https://www.braintrust.dev/app/Test%20Org%20With%20Spaces/p/Test%20Project%20With%20Spaces/experiments/test%20experiment"); + } +}