Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
68 changes: 59 additions & 9 deletions src/main/java/dev/braintrust/api/Experiment.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,13 +25,7 @@ private Experiment() {
public static CompletableFuture<BraintrustApiClient.Experiment> 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);
}

/**
Expand All @@ -48,7 +44,22 @@ public static CompletableFuture<BraintrustApiClient.Experiment> 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;
});
});
}

/**
Expand Down Expand Up @@ -88,7 +99,12 @@ public static CompletableFuture<String> 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());
}
Expand Down Expand Up @@ -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());
}
}
}
116 changes: 116 additions & 0 deletions src/test/java/dev/braintrust/api/ExperimentTest.java
Original file line number Diff line number Diff line change
@@ -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");
}
}