diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/TraceBatch.java b/apps/opik-backend/src/main/java/com/comet/opik/api/TraceBatch.java new file mode 100644 index 0000000000..ac5c164940 --- /dev/null +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/TraceBatch.java @@ -0,0 +1,12 @@ +package com.comet.opik.api; + +import com.fasterxml.jackson.annotation.JsonView; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.util.List; + +public record TraceBatch(@NotNull @Size(min = 1, max = 1000) @JsonView( { + Trace.View.Write.class}) @Valid List traces){ +} diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TraceResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TraceResource.java index d5843cac98..bd1c77aca1 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TraceResource.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TraceResource.java @@ -5,6 +5,7 @@ import com.comet.opik.api.FeedbackScore; import com.comet.opik.api.FeedbackScoreBatch; import com.comet.opik.api.Trace; +import com.comet.opik.api.TraceBatch; import com.comet.opik.api.TraceSearchCriteria; import com.comet.opik.api.TraceUpdate; import com.comet.opik.api.filter.FiltersFactory; @@ -25,6 +26,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.ClientErrorException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DefaultValue; @@ -45,6 +47,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.UUID; +import java.util.stream.Collectors; import static com.comet.opik.utils.AsyncUtils.setRequestContext; import static com.comet.opik.utils.ValidationUtils.validateProjectNameAndProjectId; @@ -115,6 +118,30 @@ public Response create( return Response.created(uri).build(); } + @POST + @Path("/batch") + @Operation(operationId = "createTraces", summary = "Create traces", description = "Create traces", responses = { + @ApiResponse(responseCode = "204", description = "No Content")}) + public Response createSpans( + @RequestBody(content = @Content(schema = @Schema(implementation = TraceBatch.class))) @JsonView(Trace.View.Write.class) @NotNull @Valid TraceBatch traces) { + + traces.traces() + .stream() + .filter(trace -> trace.id() != null) // Filter out spans with null IDs + .collect(Collectors.groupingBy(Trace::id)) + .forEach((id, traceGroup) -> { + if (traceGroup.size() > 1) { + throw new ClientErrorException("Duplicate trace id '%s'".formatted(id), 422); + } + }); + + service.create(traces) + .contextWrite(ctx -> setRequestContext(ctx, requestContext)) + .block(); + + return Response.noContent().build(); + } + @PATCH @Path("{id}") @Operation(operationId = "updateTrace", summary = "Update trace by id", description = "Update trace by id", responses = { diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/FeedbackScoreService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/FeedbackScoreService.java index dae9a80750..6cb1084da8 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/FeedbackScoreService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/FeedbackScoreService.java @@ -37,7 +37,6 @@ @ImplementedBy(FeedbackScoreServiceImpl.class) public interface FeedbackScoreService { - Flux getScores(EntityType entityType, UUID entityId); Mono scoreTrace(UUID traceId, FeedbackScore score); Mono scoreSpan(UUID spanId, FeedbackScore score); @@ -66,12 +65,6 @@ class FeedbackScoreServiceImpl implements FeedbackScoreService { record ProjectDto(Project project, List scores) { } - @Override - public Flux getScores(@NonNull EntityType entityType, @NonNull UUID entityId) { - return asyncTemplate.nonTransaction(connection -> dao.getScores(entityType, List.of(entityId), connection)) - .flatMapIterable(entityIdToFeedbackScoresMap -> entityIdToFeedbackScoresMap.get(entityId)); - } - @Override public Mono scoreTrace(@NonNull UUID traceId, @NonNull FeedbackScore score) { return lockService.executeWithLock( diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java index 256daa3ad3..5a054f3245 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java @@ -6,6 +6,9 @@ import com.comet.opik.domain.filter.FilterQueryBuilder; import com.comet.opik.domain.filter.FilterStrategy; import com.comet.opik.utils.JsonUtils; +import com.comet.opik.utils.TemplateUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Preconditions; import com.google.inject.ImplementedBy; import com.newrelic.api.agent.Segment; import io.r2dbc.spi.Connection; @@ -38,6 +41,7 @@ import static com.comet.opik.infrastructure.instrumentation.InstrumentAsyncUtils.startSegment; import static com.comet.opik.utils.AsyncUtils.makeFluxContextAware; import static com.comet.opik.utils.AsyncUtils.makeMonoContextAware; +import static com.comet.opik.utils.TemplateUtils.getQueryItemPlaceHolder; @ImplementedBy(TraceDAOImpl.class) interface TraceDAO { @@ -57,6 +61,7 @@ Mono partialInsert(UUID projectId, TraceUpdate traceUpdate, UUID traceId, Mono> getTraceWorkspace(Set traceIds, Connection connection); + Mono batchInsert(List traces, Connection connection); } @Slf4j @@ -64,6 +69,41 @@ Mono partialInsert(UUID projectId, TraceUpdate traceUpdate, UUID traceId, @RequiredArgsConstructor(onConstructor_ = @Inject) class TraceDAOImpl implements TraceDAO { + private static final String BATCH_INSERT = """ + INSERT INTO traces( + id, + project_id, + workspace_id, + name, + start_time, + end_time, + input, + output, + metadata, + tags, + created_by, + last_updated_by + ) VALUES + , + :project_id, + :workspace_id, + :name, + parseDateTime64BestEffort(:start_time, 9), + if(:end_time IS NULL, NULL, parseDateTime64BestEffort(:end_time, 9)), + :input, + :output, + :metadata, + :tags, + :user_name, + :user_name + ) + , + }> + ; + """; + /** * This query handles the insertion of a new trace into the database in two cases: * 1. When the trace does not exist in the database. @@ -695,4 +735,61 @@ public Mono> getTraceWorkspace( .collectList(); } + @Override + public Mono batchInsert(@NonNull List traces, @NonNull Connection connection) { + + Preconditions.checkArgument(!traces.isEmpty(), "traces must not be empty"); + + return Mono.from(insert(traces, connection)) + .flatMapMany(Result::getRowsUpdated) + .reduce(0L, Long::sum); + + } + + private Publisher insert(List traces, Connection connection) { + + return makeMonoContextAware((userName, workspaceName, workspaceId) -> { + List queryItems = getQueryItemPlaceHolder(traces.size()); + + var template = new ST(BATCH_INSERT) + .add("items", queryItems); + + Statement statement = connection.createStatement(template.render()); + + int i = 0; + for (Trace trace : traces) { + + statement.bind("id" + i, trace.id()) + .bind("project_id" + i, trace.projectId()) + .bind("name" + i, trace.name()) + .bind("start_time" + i, trace.startTime().toString()) + .bind("input" + i, getOrDefault(trace.input())) + .bind("output" + i, getOrDefault(trace.output())) + .bind("metadata" + i, getOrDefault(trace.metadata())) + .bind("tags" + i, trace.tags() != null ? trace.tags().toArray(String[]::new) : new String[]{}); + + if (trace.endTime() != null) { + statement.bind("end_time" + i, trace.endTime().toString()); + } else { + statement.bindNull("end_time" + i, String.class); + } + + i++; + } + + statement + .bind("workspace_id", workspaceId) + .bind("user_name", userName); + + Segment segment = startSegment("traces", "Clickhouse", "batch_insert"); + + return Mono.from(statement.execute()) + .doFinally(signalType -> endSegment(segment)); + }); + } + + private String getOrDefault(JsonNode value) { + return value != null ? value.toString() : ""; + } + } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceService.java index 3dd3d7c590..77002bf5ca 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceService.java @@ -3,6 +3,7 @@ import com.clickhouse.client.ClickHouseException; import com.comet.opik.api.Project; import com.comet.opik.api.Trace; +import com.comet.opik.api.TraceBatch; import com.comet.opik.api.TraceSearchCriteria; import com.comet.opik.api.TraceUpdate; import com.comet.opik.api.error.EntityAlreadyExistsException; @@ -13,6 +14,7 @@ import com.comet.opik.infrastructure.redis.LockService; import com.comet.opik.utils.AsyncUtils; import com.comet.opik.utils.WorkspaceUtils; +import com.google.common.base.Preconditions; import com.google.inject.ImplementedBy; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -20,13 +22,17 @@ import jakarta.ws.rs.core.Response; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; import static com.comet.opik.domain.FeedbackScoreDAO.EntityType; @@ -35,6 +41,8 @@ public interface TraceService { Mono create(Trace trace); + Mono create(TraceBatch batch); + Mono update(TraceUpdate trace, UUID id); Mono get(UUID id); @@ -77,6 +85,49 @@ public Mono create(@NonNull Trace trace) { Mono.defer(() -> insertTrace(trace, project, id)))); } + @com.newrelic.api.agent.Trace(dispatcher = true) + public Mono create(TraceBatch batch) { + + Preconditions.checkArgument(!batch.traces().isEmpty(), "Batch traces cannot be empty"); + + List projectNames = batch.traces() + .stream() + .map(Trace::projectName) + .distinct() + .toList(); + + Mono> resolveProjects = Flux.fromIterable(projectNames) + .flatMap(this::resolveProject) + .collectList() + .map(projects -> bindTraceToProjectAndId(batch, projects)) + .subscribeOn(Schedulers.boundedElastic()); + + return resolveProjects + .flatMap(traces -> template.nonTransaction(connection -> dao.batchInsert(traces, connection))); + } + + private List bindTraceToProjectAndId(TraceBatch batch, List projects) { + Map projectPerName = projects.stream() + .collect(Collectors.toMap(Project::name, Function.identity())); + + return batch.traces() + .stream() + .map(trace -> { + String projectName = WorkspaceUtils.getProjectName(trace.projectName()); + Project project = projectPerName.get(projectName); + + UUID id = trace.id() == null ? idGenerator.generateId() : trace.id(); + IdGenerator.validateVersion(id, TRACE_KEY); + + return trace.toBuilder().id(id).projectId(project.id()).build(); + }) + .toList(); + } + + private Mono resolveProject(String projectName) { + return getOrCreateProject(WorkspaceUtils.getProjectName(projectName)); + } + private Mono insertTrace(Trace newTrace, Project project, UUID id) { //TODO: refactor to implement proper conflict resolution return template.nonTransaction(connection -> dao.findById(id, connection)) diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/TestUtils.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/TestUtils.java new file mode 100644 index 0000000000..99d5fd1d33 --- /dev/null +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/TestUtils.java @@ -0,0 +1,14 @@ +package com.comet.opik.api.resources.utils; + +import lombok.experimental.UtilityClass; + +import java.net.URI; +import java.util.UUID; + +@UtilityClass +public class TestUtils { + + public static UUID getIdFromLocation(URI location) { + return UUID.fromString(location.getPath().substring(location.getPath().lastIndexOf('/') + 1)); + } +} diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java index 79e6cb9a51..537d8ccc94 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceTest.java @@ -24,6 +24,7 @@ import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; import com.comet.opik.api.resources.utils.TestDropwizardAppExtensionUtils; +import com.comet.opik.api.resources.utils.TestUtils; import com.comet.opik.api.resources.utils.WireMockUtils; import com.comet.opik.domain.FeedbackScoreMapper; import com.comet.opik.podam.PodamFactoryUtils; @@ -195,8 +196,7 @@ private UUID createAndAssert(Dataset dataset, String apiKey, String workspaceNam assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); assertThat(actualResponse.hasEntity()).isFalse(); - var id = UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + var id = TestUtils.getIdFromLocation(actualResponse.getLocation()); assertThat(id).isNotNull(); assertThat(id.version()).isEqualTo(7); @@ -2532,8 +2532,7 @@ private UUID createTrace(Trace trace, String apiKey, String workspaceName) { .post(Entity.entity(trace, MediaType.APPLICATION_JSON_TYPE))) { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - return UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + return TestUtils.getIdFromLocation(actualResponse.getLocation()); } } @@ -2545,8 +2544,7 @@ private UUID createSpan(Span span, String apiKey, String workspaceName) { .post(Entity.entity(span, MediaType.APPLICATION_JSON_TYPE))) { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - return UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + return TestUtils.getIdFromLocation(actualResponse.getLocation()); } } diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceTest.java index 3880b5eb96..2beeb2df2b 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceTest.java @@ -18,6 +18,7 @@ import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; import com.comet.opik.api.resources.utils.TestDropwizardAppExtensionUtils; +import com.comet.opik.api.resources.utils.TestUtils; import com.comet.opik.api.resources.utils.WireMockUtils; import com.comet.opik.domain.FeedbackScoreMapper; import com.comet.opik.podam.PodamFactoryUtils; @@ -1536,8 +1537,7 @@ private UUID createAndAssert(Experiment expectedExperiment, String apiKey, Strin assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - var path = actualResponse.getLocation().getPath(); - var actualId = UUID.fromString(path.substring(path.lastIndexOf('/') + 1)); + var actualId = TestUtils.getIdFromLocation(actualResponse.getLocation()); assertThat(actualResponse.hasEntity()).isFalse(); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FeedbackDefinitionResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FeedbackDefinitionResourceTest.java index 27f543ce41..5390a9f49c 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FeedbackDefinitionResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FeedbackDefinitionResourceTest.java @@ -8,6 +8,7 @@ import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; import com.comet.opik.api.resources.utils.TestDropwizardAppExtensionUtils; +import com.comet.opik.api.resources.utils.TestUtils; import com.comet.opik.api.resources.utils.WireMockUtils; import com.comet.opik.podam.PodamFactoryUtils; import com.fasterxml.uuid.Generators; @@ -135,8 +136,7 @@ private UUID create(final FeedbackDefinition feedback, String apiKey, String assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - return UUID.fromString(actualResponse.getLocation().getPath() - .substring(actualResponse.getLocation().getPath().lastIndexOf('/') + 1)); + return TestUtils.getIdFromLocation(actualResponse.getLocation()); } } @@ -848,9 +848,7 @@ void create() { assertThat(actualResponse.hasEntity()).isFalse(); assertThat(actualResponse.getHeaderString("Location")).matches(Pattern.compile(URL_PATTERN)); - id = UUID.fromString(actualResponse.getLocation().getPath() - .substring(actualResponse.getLocation().getPath().lastIndexOf('/') + 1)); - + id = TestUtils.getIdFromLocation(actualResponse.getLocation()); } var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java index 117fabcd7d..ea8b879d8a 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectsResourceTest.java @@ -9,6 +9,7 @@ import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; import com.comet.opik.api.resources.utils.TestDropwizardAppExtensionUtils; +import com.comet.opik.api.resources.utils.TestUtils; import com.comet.opik.api.resources.utils.WireMockUtils; import com.comet.opik.podam.PodamFactoryUtils; import com.github.tomakehurst.wiremock.client.WireMock; @@ -134,8 +135,7 @@ private UUID createProject(Project project, String apiKey, String workspaceName) assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - return UUID.fromString(actualResponse.getLocation().getPath() - .substring(actualResponse.getLocation().getPath().lastIndexOf('/') + 1)); + return TestUtils.getIdFromLocation(actualResponse.getLocation()); } } @@ -820,8 +820,7 @@ void create() { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); assertThat(actualResponse.hasEntity()).isFalse(); - id = UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + id = TestUtils.getIdFromLocation(actualResponse.getLocation()); } assertProject(project.toBuilder().id(id) @@ -848,8 +847,7 @@ void create__whenWorkspaceNameIsSpecified__thenAcceptTheRequest() { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); assertThat(actualResponse.hasEntity()).isFalse(); - id = UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + id = TestUtils.getIdFromLocation(actualResponse.getLocation()); } @@ -932,8 +930,7 @@ void create__whenProjectsHaveSameNameButDifferentWorkspace__thenAcceptTheRequest assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); assertThat(actualResponse.hasEntity()).isFalse(); - id = UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + id = TestUtils.getIdFromLocation(actualResponse.getLocation()); } var project2 = project1.toBuilder() @@ -950,8 +947,7 @@ void create__whenProjectsHaveSameNameButDifferentWorkspace__thenAcceptTheRequest assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); assertThat(actualResponse.hasEntity()).isFalse(); - id2 = UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + id2 = TestUtils.getIdFromLocation(actualResponse.getLocation()); } assertProject(project1.toBuilder().id(id).build()); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java index 60db8e83ce..028c1c3c75 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java @@ -22,6 +22,7 @@ import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; import com.comet.opik.api.resources.utils.TestDropwizardAppExtensionUtils; +import com.comet.opik.api.resources.utils.TestUtils; import com.comet.opik.api.resources.utils.WireMockUtils; import com.comet.opik.domain.SpanMapper; import com.comet.opik.domain.SpanType; @@ -184,8 +185,7 @@ private UUID createProject(String projectName, String workspaceName, String apiK .post(Entity.json(Project.builder().name(projectName).build()))) { assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(201); - return UUID.fromString( - response.getLocation().getPath().substring(response.getLocation().getPath().lastIndexOf('/') + 1)); + return TestUtils.getIdFromLocation(response.getLocation()); } } @@ -3107,7 +3107,7 @@ private UUID createAndAssert(Span expectedSpan, String apiKey, String workspaceN if (expectedSpan.id() != null) { expectedSpanId = expectedSpan.id(); } else { - expectedSpanId = UUID.fromString(actualHeaderString.substring(actualHeaderString.lastIndexOf('/') + 1)); + expectedSpanId = TestUtils.getIdFromLocation(actualResponse.getLocation()); } assertThat(actualHeaderString).isEqualTo(URL_TEMPLATE.formatted(baseURI) diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java index 74687bfee5..aa5e155592 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java @@ -7,6 +7,7 @@ import com.comet.opik.api.Project; import com.comet.opik.api.ScoreSource; import com.comet.opik.api.Trace; +import com.comet.opik.api.TraceBatch; import com.comet.opik.api.TraceUpdate; import com.comet.opik.api.error.ErrorMessage; import com.comet.opik.api.filter.Filter; @@ -21,6 +22,7 @@ import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; import com.comet.opik.api.resources.utils.TestDropwizardAppExtensionUtils; +import com.comet.opik.api.resources.utils.TestUtils; import com.comet.opik.api.resources.utils.WireMockUtils; import com.comet.opik.infrastructure.auth.RequestContext; import com.comet.opik.podam.PodamFactoryUtils; @@ -162,7 +164,7 @@ void tearDownAll() { wireMock.server().stop(); } - private UUID getProjectId(ClientSupport client, String projectName, String workspaceName, String apiKey) { + private UUID getProjectId(String projectName, String workspaceName, String apiKey) { return client.target("%s/v1/private/projects".formatted(baseURI)) .queryParam("name", projectName) .request() @@ -177,6 +179,19 @@ private UUID getProjectId(ClientSupport client, String projectName, String works .id(); } + private UUID createProject(String projectName, String workspaceName, String apiKey) { + try (Response response = client.target("%s/v1/private/projects".formatted(baseURI)) + .queryParam("name", projectName) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .header(WORKSPACE_HEADER, workspaceName) + .post(Entity.json(Project.builder().name(projectName).build()))) { + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(201); + return TestUtils.getIdFromLocation(response.getLocation()); + } + } + @Nested @DisplayName("Api Key Authentication:") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -837,7 +852,7 @@ void getByProjectName__whenProjectIdIsNotEmpty__thenReturnTracesByProjectId() { .feedbackScores(null) .build(), apiKey, workspaceName); - UUID projectId = getProjectId(client, projectName, workspaceName, apiKey); + UUID projectId = getProjectId(projectName, workspaceName, apiKey); var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) .queryParam("workspace_name", workspaceName) @@ -2635,93 +2650,96 @@ void getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Fi var actualError = actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); assertThat(actualError).isEqualTo(expectedError); } + } - private void getAndAssertPage(String workspaceName, String projectName, List filters, - List traces, - List expectedTraces, List unexpectedTraces, String apiKey) { - int page = 1; - int size = traces.size() + expectedTraces.size() + unexpectedTraces.size(); - getAndAssertPage(page, size, projectName, filters, expectedTraces, unexpectedTraces, - workspaceName, apiKey); - } + private void getAndAssertPage(String workspaceName, String projectName, List filters, + List traces, + List expectedTraces, List unexpectedTraces, String apiKey) { + int page = 1; + int size = traces.size() + expectedTraces.size() + unexpectedTraces.size(); + getAndAssertPage(page, size, projectName, filters, expectedTraces, unexpectedTraces, + workspaceName, apiKey); + } - private void getAndAssertPage(int page, int size, String projectName, List filters, - List expectedTraces, List unexpectedTraces, String workspaceName, String apiKey) { - var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) - .queryParam("page", page) - .queryParam("size", size) - .queryParam("project_name", projectName) - .queryParam("filters", toURLEncodedQueryParam(filters)) - .request() - .header(HttpHeaders.AUTHORIZATION, apiKey) - .header(WORKSPACE_HEADER, workspaceName) - .get(); + private void getAndAssertPage(int page, int size, String projectName, List filters, + List expectedTraces, List unexpectedTraces, String workspaceName, String apiKey) { + var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + .queryParam("page", page) + .queryParam("size", size) + .queryParam("project_name", projectName) + .queryParam("filters", toURLEncodedQueryParam(filters)) + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .header(WORKSPACE_HEADER, workspaceName) + .get(); - assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); - var actualPage = actualResponse.readEntity(Trace.TracePage.class); - var actualTraces = actualPage.content(); + var actualPage = actualResponse.readEntity(Trace.TracePage.class); + var actualTraces = actualPage.content(); - assertThat(actualPage.page()).isEqualTo(page); - assertThat(actualPage.size()).isEqualTo(expectedTraces.size()); - assertThat(actualPage.total()).isEqualTo(expectedTraces.size()); - assertThat(actualTraces) - .usingRecursiveFieldByFieldElementComparatorIgnoringFields(IGNORED_FIELDS_LIST) - .containsExactlyElementsOf(expectedTraces); - assertIgnoredFields(actualTraces, expectedTraces); + assertThat(actualPage.page()).isEqualTo(page); + assertThat(actualPage.size()).isEqualTo(expectedTraces.size()); + assertThat(actualPage.total()).isEqualTo(expectedTraces.size()); + assertThat(actualTraces) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields(IGNORED_FIELDS_LIST) + .containsExactlyElementsOf(expectedTraces); + assertIgnoredFields(actualTraces, expectedTraces); + + if (!unexpectedTraces.isEmpty()) { assertThat(actualTraces) .usingRecursiveFieldByFieldElementComparatorIgnoringFields(IGNORED_FIELDS_LIST) .doesNotContainAnyElementsOf(unexpectedTraces); } + } - private String toURLEncodedQueryParam(List filters) { - return URLEncoder.encode(JsonUtils.writeValueAsString(filters), StandardCharsets.UTF_8); - } + private String toURLEncodedQueryParam(List filters) { + return URLEncoder.encode(JsonUtils.writeValueAsString(filters), StandardCharsets.UTF_8); + } - private void assertIgnoredFields(List actualTraces, List expectedTraces) { - for (int i = 0; i < actualTraces.size(); i++) { - var actualTrace = actualTraces.get(i); - var expectedTrace = expectedTraces.get(i); - var expectedFeedbackScores = expectedTrace.feedbackScores() == null - ? null - : expectedTrace.feedbackScores().reversed(); - assertThat(actualTrace.projectId()).isNotNull(); - assertThat(actualTrace.projectName()).isNull(); - assertThat(actualTrace.createdAt()).isAfter(expectedTrace.createdAt()); - assertThat(actualTrace.lastUpdatedAt()).isAfter(expectedTrace.lastUpdatedAt()); - assertThat(actualTrace.lastUpdatedBy()).isEqualTo(USER); - assertThat(actualTrace.lastUpdatedBy()).isEqualTo(USER); - assertThat(actualTrace.feedbackScores()) - .usingRecursiveComparison( - RecursiveComparisonConfiguration.builder() - .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) - .withIgnoredFields(IGNORED_FIELDS) - .build()) - .isEqualTo(expectedFeedbackScores); - - if (expectedTrace.feedbackScores() != null) { - actualTrace.feedbackScores().forEach(feedbackScore -> { - assertThat(feedbackScore.createdAt()).isAfter(expectedTrace.createdAt()); - assertThat(feedbackScore.lastUpdatedAt()).isAfter(expectedTrace.createdAt()); - assertThat(feedbackScore.lastUpdatedBy()).isEqualTo(USER); - assertThat(feedbackScore.lastUpdatedBy()).isEqualTo(USER); - }); - } + private void assertIgnoredFields(List actualTraces, List expectedTraces) { + for (int i = 0; i < actualTraces.size(); i++) { + var actualTrace = actualTraces.get(i); + var expectedTrace = expectedTraces.get(i); + var expectedFeedbackScores = expectedTrace.feedbackScores() == null + ? null + : expectedTrace.feedbackScores().reversed(); + assertThat(actualTrace.projectId()).isNotNull(); + assertThat(actualTrace.projectName()).isNull(); + assertThat(actualTrace.createdAt()).isAfter(expectedTrace.createdAt()); + assertThat(actualTrace.lastUpdatedAt()).isAfter(expectedTrace.lastUpdatedAt()); + assertThat(actualTrace.lastUpdatedBy()).isEqualTo(USER); + assertThat(actualTrace.lastUpdatedBy()).isEqualTo(USER); + assertThat(actualTrace.feedbackScores()) + .usingRecursiveComparison( + RecursiveComparisonConfiguration.builder() + .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) + .withIgnoredFields(IGNORED_FIELDS) + .build()) + .isEqualTo(expectedFeedbackScores); + + if (expectedTrace.feedbackScores() != null) { + actualTrace.feedbackScores().forEach(feedbackScore -> { + assertThat(feedbackScore.createdAt()).isAfter(expectedTrace.createdAt()); + assertThat(feedbackScore.lastUpdatedAt()).isAfter(expectedTrace.createdAt()); + assertThat(feedbackScore.lastUpdatedBy()).isEqualTo(USER); + assertThat(feedbackScore.lastUpdatedBy()).isEqualTo(USER); + }); } } + } - private List updateFeedbackScore(List feedbackScores, int index, double val) { - feedbackScores.set(index, feedbackScores.get(index).toBuilder() - .value(BigDecimal.valueOf(val)) - .build()); - return feedbackScores; - } + private List updateFeedbackScore(List feedbackScores, int index, double val) { + feedbackScores.set(index, feedbackScores.get(index).toBuilder() + .value(BigDecimal.valueOf(val)) + .build()); + return feedbackScores; + } - private List updateFeedbackScore( - List destination, List source, int index) { - destination.set(index, source.get(index).toBuilder().build()); - return destination; - } + private List updateFeedbackScore( + List destination, List source, int index) { + destination.set(index, source.get(index).toBuilder().build()); + return destination; } @Nested @@ -2808,8 +2826,7 @@ private UUID create(Trace trace, String apiKey, String workspaceName) { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - return UUID.fromString(actualResponse.getHeaderString("Location") - .substring(actualResponse.getHeaderString("Location").lastIndexOf('/') + 1)); + return TestUtils.getIdFromLocation(actualResponse.getLocation()); } } @@ -2893,7 +2910,7 @@ void create() { assertThat(actualResponse.getHeaderString("Location")).matches(Pattern.compile(URL_PATTERN)); } - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); getAndAssert(trace, id, projectId, now, API_KEY, TEST_WORKSPACE); } @@ -2921,8 +2938,8 @@ void create__whenCreatingTracesWithDifferentWorkspacesNames__thenReturnCreatedTr var createdTrace2 = Instant.now(); UUID id2 = TracesResourceTest.this.create(trace2, API_KEY, TEST_WORKSPACE); - UUID projectId1 = getProjectId(client, DEFAULT_PROJECT, TEST_WORKSPACE, API_KEY); - UUID projectId2 = getProjectId(client, projectName, TEST_WORKSPACE, API_KEY); + UUID projectId1 = getProjectId(DEFAULT_PROJECT, TEST_WORKSPACE, API_KEY); + UUID projectId2 = getProjectId(projectName, TEST_WORKSPACE, API_KEY); getAndAssert(trace1, id1, projectId1, createdTrace1, API_KEY, TEST_WORKSPACE); getAndAssert(trace2, id2, projectId2, createdTrace2, API_KEY, TEST_WORKSPACE); @@ -2953,10 +2970,9 @@ void create__whenIdComesFromClient__thenAcceptAndUseId() { assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - String actualId = actualResponse.getLocation().toString() - .substring(actualResponse.getLocation().toString().lastIndexOf('/') + 1); + UUID actualId = TestUtils.getIdFromLocation(actualResponse.getLocation()); - assertThat(UUID.fromString(actualId)).isEqualTo(traceId); + assertThat(actualId).isEqualTo(traceId); } } @@ -3027,7 +3043,7 @@ void create__whenProjectNameIsNull__thenAcceptAndUseDefaultProject() { var actualResponse = getById(id, TEST_WORKSPACE, API_KEY); assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(200); - UUID projectId = getProjectId(client, DEFAULT_PROJECT, TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(DEFAULT_PROJECT, TEST_WORKSPACE, API_KEY); var actualEntity = actualResponse.readEntity(Trace.class); assertThat(actualEntity.projectId()).isEqualTo(projectId); @@ -3035,6 +3051,128 @@ void create__whenProjectNameIsNull__thenAcceptAndUseDefaultProject() { } + @Nested + @DisplayName("Batch:") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class BatchInsert { + + @Test + void batch__whenCreateTraces__thenReturnNoContent() { + + String projectName = UUID.randomUUID().toString(); + + UUID projectId = createProject(projectName, TEST_WORKSPACE, API_KEY); + + var expectedTraces = IntStream.range(0, 1000) + .mapToObj(i -> factory.manufacturePojo(Trace.class).toBuilder() + .projectName(projectName) + .projectId(projectId) + .endTime(null) + .feedbackScores(null) + .build()) + .toList(); + + batchCreateAndAssert(expectedTraces, API_KEY, TEST_WORKSPACE); + + getAndAssertPage(TEST_WORKSPACE, projectName, List.of(), List.of(), expectedTraces.reversed(), List.of(), + API_KEY); + } + + @Test + void batch__whenSendingMultipleTracesWithSameId__thenReturn422() { + var trace = factory.manufacturePojo(Trace.class).toBuilder() + .projectId(null) + .feedbackScores(null) + .build(); + + var expectedTrace = trace.toBuilder() + .tags(Set.of()) + .endTime(Instant.now()) + .output(JsonUtils.getJsonNodeFromString("{ \"output\": \"data\"}")) + .build(); + + List traces = List.of(trace, expectedTrace); + + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + .path("batch") + .request() + .header(HttpHeaders.AUTHORIZATION, API_KEY) + .header(WORKSPACE_HEADER, TEST_WORKSPACE) + .post(Entity.json(new TraceBatch(traces)))) { + + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + assertThat(actualResponse.hasEntity()).isTrue(); + + var errorMessage = actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); + assertThat(errorMessage.getMessage()).isEqualTo("Duplicate trace id '%s'".formatted(trace.id())); + } + } + + @ParameterizedTest + @MethodSource + void batch__whenBatchIsInvalid__thenReturn422(List traces, String errorMessage) { + + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + .path("batch") + .request() + .header(HttpHeaders.AUTHORIZATION, API_KEY) + .header(WORKSPACE_HEADER, TEST_WORKSPACE) + .post(Entity.json(new TraceBatch(traces)))) { + + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(422); + assertThat(actualResponse.hasEntity()).isTrue(); + + var responseBody = actualResponse.readEntity(ErrorMessage.class); + assertThat(responseBody.errors()).contains(errorMessage); + } + } + + Stream batch__whenBatchIsInvalid__thenReturn422() { + return Stream.of( + Arguments.of(List.of(), "traces size must be between 1 and 1000"), + Arguments.of(IntStream.range(0, 1001) + .mapToObj(i -> factory.manufacturePojo(Trace.class).toBuilder() + .projectId(null) + .feedbackScores(null) + .build()) + .toList(), "traces size must be between 1 and 1000")); + } + + @Test + void batch__whenSendingMultipleTracesWithNoId__thenReturnNoContent() { + var newTrace = factory.manufacturePojo(Trace.class).toBuilder() + .projectId(null) + .id(null) + .feedbackScores(null) + .build(); + + var expectedTrace = newTrace.toBuilder() + .tags(Set.of()) + .endTime(Instant.now()) + .output(JsonUtils.getJsonNodeFromString("{ \"output\": \"data\"}")) + .build(); + + List expectedTraces = List.of(newTrace, expectedTrace); + + batchCreateAndAssert(expectedTraces, API_KEY, TEST_WORKSPACE); + } + + private void batchCreateAndAssert(List traces, String apiKey, String workspaceName) { + + try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) + .path("batch") + .request() + .header(HttpHeaders.AUTHORIZATION, apiKey) + .header(WORKSPACE_HEADER, workspaceName) + .post(Entity.json(new TraceBatch(traces)))) { + + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(204); + assertThat(actualResponse.hasEntity()).isFalse(); + } + } + + } + @Nested @DisplayName("Delete:") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -3157,7 +3295,7 @@ void when__traceDoesNotExist__thenReturnCreateIt() { assertThat(actualEntity.metadata()).isEqualTo(traceUpdate.metadata()); assertThat(actualEntity.tags()).isEqualTo(traceUpdate.tags()); - UUID projectId = getProjectId(client, traceUpdate.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(traceUpdate.projectName(), TEST_WORKSPACE, API_KEY); assertThat(actualEntity.name()).isEmpty(); assertThat(actualEntity.startTime()).isEqualTo(Instant.EPOCH); @@ -3375,7 +3513,7 @@ void update__whenTagsIsEmpty__thenAcceptUpdate() { runPatchAndAssertStatus(id, traceUpdate, API_KEY, TEST_WORKSPACE); - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); Trace actualTrace = getAndAssert(trace, id, projectId, trace.createdAt().minusMillis(1), API_KEY, TEST_WORKSPACE); @@ -3396,7 +3534,7 @@ void update__whenMetadataIsEmpty__thenAcceptUpdate() { runPatchAndAssertStatus(id, traceUpdate, API_KEY, TEST_WORKSPACE); - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); Trace actualTrace = getAndAssert(trace.toBuilder().metadata(metadata).build(), id, projectId, trace.createdAt().minusMillis(1), API_KEY, TEST_WORKSPACE); @@ -3417,7 +3555,7 @@ void update__whenInputIsEmpty__thenAcceptUpdate() { runPatchAndAssertStatus(id, traceUpdate, API_KEY, TEST_WORKSPACE); - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); Trace actualTrace = getAndAssert(trace.toBuilder().input(input).build(), id, projectId, trace.createdAt().minusMillis(1), API_KEY, TEST_WORKSPACE); @@ -3438,7 +3576,7 @@ void update__whenOutputIsEmpty__thenAcceptUpdate() { runPatchAndAssertStatus(id, traceUpdate, API_KEY, TEST_WORKSPACE); - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); Trace actualTrace = getAndAssert(trace.toBuilder().output(output).build(), id, projectId, trace.createdAt().minusMillis(1), API_KEY, TEST_WORKSPACE); @@ -3450,7 +3588,7 @@ void update__whenOutputIsEmpty__thenAcceptUpdate() { @DisplayName("when updating using projectId, then accept update") void update__whenUpdatingUsingProjectId__thenAcceptUpdate() { - var projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + var projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); var traceUpdate = factory.manufacturePojo(TraceUpdate.class).toBuilder() .projectId(projectId) @@ -3578,7 +3716,7 @@ void feedback__whenFeedbackWithoutCategoryNameOrReason__thenReturnNoContent() { create(id, score, TEST_WORKSPACE, API_KEY); - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); var actualEntity = getAndAssert(trace, id, projectId, now, API_KEY, TEST_WORKSPACE); @@ -3615,7 +3753,7 @@ void feedback__whenFeedbackWithCategoryNameOrReason__thenReturnNoContent() { create(id, score, TEST_WORKSPACE, API_KEY); - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); Trace actualEntity = getAndAssert(trace, id, projectId, now, API_KEY, TEST_WORKSPACE); @@ -3653,7 +3791,7 @@ void feedback__whenOverridingFeedbackValue__thenReturnNoContent() { FeedbackScore newScore = score.toBuilder().value(BigDecimal.valueOf(2)).build(); create(id, newScore, TEST_WORKSPACE, API_KEY); - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); var actualEntity = getAndAssert(trace, id, projectId, now, API_KEY, TEST_WORKSPACE); assertThat(actualEntity.feedbackScores()).hasSize(1); @@ -3846,8 +3984,8 @@ void feedback() { assertThat(actualResponse.hasEntity()).isFalse(); } - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); - UUID projectId2 = getProjectId(client, trace2.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId2 = getProjectId(trace2.projectName(), TEST_WORKSPACE, API_KEY); var actualTrace1 = getAndAssert(trace, id, projectId, now, API_KEY, TEST_WORKSPACE); var actualTrace2 = getAndAssert(trace2, id2, projectId2, now, API_KEY, TEST_WORKSPACE); @@ -3917,8 +4055,8 @@ void feedback__whenWorkspaceIsSpecified__thenReturnNoContent() { assertThat(actualResponse.hasEntity()).isFalse(); } - UUID projectId = getProjectId(client, DEFAULT_PROJECT, workspaceName, apiKey); - UUID projectId2 = getProjectId(client, projectName, workspaceName, apiKey); + UUID projectId = getProjectId(DEFAULT_PROJECT, workspaceName, apiKey); + UUID projectId2 = getProjectId(projectName, workspaceName, apiKey); var actualTrace1 = getAndAssert(expectedTrace1, id, projectId, now, apiKey, workspaceName); var actualTrace2 = getAndAssert(expectedTrace2, id2, projectId2, now, apiKey, workspaceName); @@ -3987,7 +4125,7 @@ void feedback__whenFeedbackWithoutCategoryNameOrReason__thenReturnNoContent() { assertThat(actualResponse.hasEntity()).isFalse(); } - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); var actualEntity = getAndAssert(trace, id, projectId, now, API_KEY, TEST_WORKSPACE); @@ -4038,7 +4176,7 @@ void feedback__whenFeedbackWithCategoryNameOrReason__thenReturnNoContent() { } var actualEntity = getAndAssert(expectedTrace, id, - getProjectId(client, expectedTrace.projectName(), TEST_WORKSPACE, API_KEY), now, API_KEY, + getProjectId(expectedTrace.projectName(), TEST_WORKSPACE, API_KEY), now, API_KEY, TEST_WORKSPACE); assertThat(actualEntity.feedbackScores()).hasSize(1); @@ -4100,7 +4238,7 @@ void feedback__whenOverridingFeedbackValue__thenReturnNoContent() { assertThat(actualResponse.hasEntity()).isFalse(); } - UUID projectId = getProjectId(client, trace.projectName(), TEST_WORKSPACE, API_KEY); + UUID projectId = getProjectId(trace.projectName(), TEST_WORKSPACE, API_KEY); var actualEntity = getAndAssert(trace, id, projectId, now, API_KEY, TEST_WORKSPACE);