diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AlertService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AlertService.java index 196f5e36c60..8c216be01fc 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AlertService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AlertService.java @@ -555,6 +555,8 @@ private Alert prepareAlert(Alert alert, String userName, String workspaceId) { UUID id = alert.id() == null ? idGenerator.generateId() : alert.id(); IdGenerator.validateVersion(id, "Alert"); + // projectId is persisted without an existence check here, so enforce v7 to avoid storing an orphan v4. + idGenerator.validateIdNotInFutureIfPresent(alert.projectId(), "project"); UUID webhookId = alert.webhook().id() == null ? idGenerator.generateId() : alert.webhook().id(); IdGenerator.validateVersion(webhookId, "Webhook"); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AnnotationQueueService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AnnotationQueueService.java index 7847199fa48..85150bc7da2 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AnnotationQueueService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AnnotationQueueService.java @@ -134,6 +134,10 @@ public Mono addItems(@NonNull UUID queueId, @NonNull Set itemIds) { return Mono.just(0L); } + // Queue items reference trace/thread ids (v7 by construction); enforce so the referenced-id + // policy is uniform. Past allowed — queues commonly collect older traces/threads. + itemIds.forEach(itemId -> idGenerator.validateIdNotInFuture(itemId, "AnnotationQueue item")); + return annotationQueueDAO.findQueueInfoById(queueId) .switchIfEmpty(Mono.error(createNotFoundError(queueId))) .flatMap(queue -> annotationQueueDAO.addItems(queueId, itemIds, queue.projectId())) @@ -257,6 +261,8 @@ private Mono enhancePageWithProjectNames( private AnnotationQueue prepareAnnotationQueue(AnnotationQueue annotationQueue) { UUID id = annotationQueue.id() == null ? idGenerator.generateId() : annotationQueue.id(); IdGenerator.validateVersion(id, "AnnotationQueue"); + // projectId is persisted without an existence check here, so enforce v7 to avoid storing an orphan v4. + idGenerator.validateIdNotInFutureIfPresent(annotationQueue.projectId(), "project"); log.debug("Preparing annotation queue with id '{}', name '{}', project '{}'", id, annotationQueue.name(), annotationQueue.projectId()); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/AssertionResultService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/AssertionResultService.java index d826b84d762..2eb95279520 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/AssertionResultService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/AssertionResultService.java @@ -43,6 +43,7 @@ class AssertionResultServiceImpl implements AssertionResultService { private final @NonNull AssertionResultDAO assertionResultDAO; private final @NonNull ProjectService projectService; private final @NonNull EventBus eventBus; + private final @NonNull IdGenerator idGenerator; @Override public Mono insertBatch(@NonNull EntityType entityType, @@ -63,7 +64,7 @@ public Mono saveBatch(@NonNull EntityType entityType, } // Validate up front so a bad id fails fast and independently of project-name normalisation. - assertionResults.forEach(item -> IdGenerator.validateVersion(item.entityId(), entityType.getType())); + assertionResults.forEach(item -> idGenerator.validateIdNotInFuture(item.entityId(), entityType.getType())); return Mono.deferContextual(ctx -> { String workspaceId = ctx.get(RequestContext.WORKSPACE_ID); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/CommentService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/CommentService.java index 5b5f0d3c505..f79c08ca083 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/CommentService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/CommentService.java @@ -53,6 +53,7 @@ class CommentServiceImpl implements CommentService { @Override public Mono create(@NonNull UUID entityId, @NonNull Comment comment, CommentDAO.EntityType entityType) { + idGenerator.validateIdNotInFuture(entityId, entityType.getType()); UUID id = idGenerator.generateId(); var monoProjectId = resolveProjectId(entityType, entityId); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/DatasetItemService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/DatasetItemService.java index e23a6ff9ebc..b746de05d6d 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/DatasetItemService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/DatasetItemService.java @@ -187,6 +187,8 @@ public Mono createFromTraces( log.info("Creating dataset items from '{}' traces for dataset '{}'", traceIds.size(), datasetId); + traceIds.forEach(traceId -> idGenerator.validateIdNotInFuture(traceId, "dataset_item trace")); + // Verify dataset exists return Mono.deferContextual(ctx -> { String workspaceId = ctx.get(RequestContext.WORKSPACE_ID); @@ -240,6 +242,8 @@ public Mono createFromSpans( log.info("Creating dataset items from '{}' spans for dataset '{}'", spanIds.size(), datasetId); + spanIds.forEach(spanId -> idGenerator.validateIdNotInFuture(spanId, "dataset_item span")); + // Verify dataset exists return Mono.deferContextual(ctx -> { String workspaceId = ctx.get(RequestContext.WORKSPACE_ID); @@ -382,6 +386,7 @@ private Mono authorizeItem(Mono itemMono) { @Override @WithSpan public Mono patch(@NonNull UUID id, @NonNull DatasetItem item) { + validateReferencedTraceAndSpan(item); return Mono.deferContextual(ctx -> { String workspaceId = ctx.get(RequestContext.WORKSPACE_ID); String userName = ctx.get(RequestContext.USER_NAME); @@ -903,11 +908,23 @@ private List addIdIfAbsent(DatasetItemBatch batch) { .stream() .map(item -> { IdGenerator.validateVersion(item.id(), "dataset_item"); + validateReferencedTraceAndSpan(item); return item; }) .toList(); } + // The dataset_item's referenced trace_id / span_id must be a time-ordered UUIDv7 (past allowed: + // items are commonly linked to older traces/spans). Reuses the shared referenced-id policy. + private void validateReferencedTraceAndSpan(DatasetItem item) { + if (item.traceId() != null) { + idGenerator.validateIdNotInFuture(item.traceId(), "dataset_item trace"); + } + if (item.spanId() != null) { + idGenerator.validateIdNotInFuture(item.spanId(), "dataset_item span"); + } + } + private Mono failWithConflict(String message) { log.info(message); return Mono.error(new IdentifierMismatchException(new ErrorMessage(List.of(message)))); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/ExperimentService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/ExperimentService.java index 3da57e5dd56..48d0a0d87eb 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/ExperimentService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/ExperimentService.java @@ -495,6 +495,8 @@ private Set getPromptVersionIds(Experiment experiment) { public Mono create(@NonNull Experiment experiment) { var id = experiment.id() == null ? idGenerator.generateId() : experiment.id(); IdGenerator.validateVersion(id, "Experiment"); + // optimizationId is stored without an existence check, so enforce v7 to avoid storing an orphan v4. + idGenerator.validateIdNotInFutureIfPresent(experiment.optimizationId(), "optimization"); var name = StringUtils.getIfBlank(experiment.name(), nameGenerator::generateName); return resolveProjectId(experiment) .flatMap(resolvedExperiment -> datasetService 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 34d6cc8f03a..787aec1faf0 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 @@ -89,6 +89,7 @@ class FeedbackScoreServiceImpl implements FeedbackScoreService { private final @NonNull TraceThreadService traceThreadService; private final @NonNull Provider requestContext; private final @NonNull EventBus eventBus; + private final @NonNull IdGenerator idGenerator; @Builder(toBuilder = true) record ProjectDto(Project project, List scores) { @@ -100,6 +101,8 @@ public Mono scoreTrace(@NonNull UUID traceId, @NonNull FeedbackScore score String workspaceId = ctx.get(RequestContext.WORKSPACE_ID); String userName = ctx.get(RequestContext.USER_NAME); + idGenerator.validateIdNotInFuture(traceId, EntityType.TRACE.getType()); + idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue"); return traceDAO.getProjectIdFromTrace(traceId) .switchIfEmpty(Mono.error(failWithNotFound("Trace", traceId))) .flatMap(projectId -> getAuthor() @@ -118,6 +121,8 @@ public Mono scoreSpan(@NonNull UUID spanId, @NonNull FeedbackScore score) String workspaceId = ctx.get(RequestContext.WORKSPACE_ID); String userName = ctx.get(RequestContext.USER_NAME); + idGenerator.validateIdNotInFuture(spanId, EntityType.SPAN.getType()); + idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue"); return spanDAO.getProjectIdFromSpan(spanId) .switchIfEmpty(Mono.error(failWithNotFound("Span", spanId))) .flatMap(projectId -> getAuthor() @@ -173,7 +178,8 @@ private Mono processScoreBatch(EntityType entityType, List> scoresPerProject = scores .stream() .map(score -> { - IdGenerator.validateVersion(score.id(), entityType.getType()); // validate span/trace id + idGenerator.validateIdNotInFuture(score.id(), entityType.getType()); // validate span/trace id + idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue"); return score.toBuilder() .projectName(WorkspaceUtils.getProjectName(score.projectName())) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/GuardrailsService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/GuardrailsService.java index c4b3405e000..df899dd553c 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/GuardrailsService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/GuardrailsService.java @@ -51,7 +51,8 @@ public Mono addTraceGuardrails(List guardrails) { .stream() .map(guardrail -> { UUID id = idGenerator.generateId(); - IdGenerator.validateVersion(guardrail.entityId(), entityType.getType()); // validate trace id + idGenerator.validateIdNotInFuture(guardrail.entityId(), entityType.getType()); + idGenerator.validateIdNotInFuture(guardrail.secondaryId(), "guardrail secondary"); return guardrail.toBuilder() .id(id) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/IdGenerator.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/IdGenerator.java index 312c85cb72d..8a754db3653 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/IdGenerator.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/IdGenerator.java @@ -34,12 +34,27 @@ public interface IdGenerator { Mono validateIdAsync(UUID id, String resource); /** - * Validates an ingested {@code id} on the update path: it must be a version 7 UUID - * ({@link #validateVersion(UUID, String)}) and must not embed a timestamp far in the future (which - * would corrupt the partition layout). Unlike {@link #validateId}, old ids are allowed, because - * updating a long-lived entity (e.g. created months ago) is a legitimate operation. + * Validates an {@code id} that may legitimately point at an entity created in the past: it must be a + * version 7 UUID ({@link #validateVersion(UUID, String)}) and must not embed a timestamp far in the + * future (which would corrupt the partition layout / retention id-range). Unlike {@link #validateId}, + * old ids are allowed. + * + *

Used both on the update path (updating a long-lived entity created months ago is legitimate) and + * for referenced/foreign ids on ingest (e.g. a span's {@code traceId}: retention orders spans by the + * {@code trace_id} range assuming it is a time-ordered UUIDv7, and late spans on old traces are common, + * so old is fine but non-v7 or future-dated must be rejected). */ - Mono validateIdForUpdateAsync(UUID id, String resource); + void validateIdNotInFuture(UUID id, String resource); + + Mono validateIdNotInFutureAsync(UUID id, String resource); + + /** + * Null-safe variant of {@link #validateIdNotInFuture} for optional referenced ids (e.g. an optional + * {@code projectId} that may be resolved by name instead). No-op when {@code id} is null. + */ + void validateIdNotInFutureIfPresent(UUID id, String resource); + + Mono validateIdNotInFutureIfPresentAsync(UUID id, String resource); static Mono validateVersionAsync(@NonNull UUID id, String resource) { return Mono.fromCallable(() -> { @@ -91,16 +106,29 @@ public Mono validateIdAsync(@NonNull UUID id, String resource) { }); } - private void validateIdForUpdate(UUID id, String resource) { + @Override + public void validateIdNotInFuture(@NonNull UUID id, String resource) { IdGenerator.validateVersion(id, resource); uuidV7TimestampValidator.validateNotInFuture(id); } @Override - public Mono validateIdForUpdateAsync(@NonNull UUID id, String resource) { + public Mono validateIdNotInFutureAsync(@NonNull UUID id, String resource) { return Mono.fromCallable(() -> { - validateIdForUpdate(id, resource); + validateIdNotInFuture(id, resource); return id; }); } + + @Override + public void validateIdNotInFutureIfPresent(UUID id, String resource) { + if (id != null) { + validateIdNotInFuture(id, resource); + } + } + + @Override + public Mono validateIdNotInFutureIfPresentAsync(UUID id, String resource) { + return id == null ? Mono.empty() : validateIdNotInFutureAsync(id, resource); + } } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanService.java index dffd115f792..276a716a3c2 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanService.java @@ -62,6 +62,8 @@ public class SpanService { public static final String PARENT_SPAN_IS_MISMATCH = "parent_span_id does not match the existing span"; public static final String TRACE_ID_MISMATCH = "trace_id does not match the existing span"; public static final String SPAN_KEY = "Span"; + public static final String SPAN_TRACE_KEY = "Span trace"; + public static final String SPAN_PARENT_KEY = "Span parent"; public static final String PROJECT_AND_WORKSPACE_NAME_MISMATCH = "Project name and workspace name do not match the existing span"; private final @NonNull SpanDAO spanDAO; @@ -159,6 +161,7 @@ public Mono create(@NonNull Span span) { var projectName = WorkspaceUtils.getProjectName(span.projectName()); return idGenerator .validateIdAsync(id, SPAN_KEY) + .then(Mono.fromRunnable(() -> validateSpanReferences(span.traceId(), span.parentSpanId()))) .then(projectService.getOrCreate(projectName)) .flatMap(project -> lockService.executeWithLock( new LockService.Lock(id, SPAN_KEY), @@ -220,7 +223,9 @@ public Mono update(@NonNull UUID id, @NonNull SpanUpdate spanUpdate) { String userName = ctx.get(RequestContext.USER_NAME); return idGenerator - .validateIdForUpdateAsync(id, SPAN_KEY) + .validateIdNotInFutureAsync(id, SPAN_KEY) + .then(Mono.fromRunnable( + () -> validateSpanReferences(spanUpdate.traceId(), spanUpdate.parentSpanId()))) .then(Mono.defer(() -> getProjectById(spanUpdate) .switchIfEmpty(Mono.defer(() -> projectService.getOrCreate(projectName))) .subscribeOn(Schedulers.boundedElastic())) @@ -247,7 +252,10 @@ public Mono batchUpdate(@NonNull SpanBatchUpdate batchUpdate) { String workspaceId = ctx.get(RequestContext.WORKSPACE_ID); String userName = ctx.get(RequestContext.USER_NAME); - return spanDAO.bulkUpdate(batchUpdate.ids(), batchUpdate.update(), mergeTags) + return Mono + .fromRunnable(() -> validateSpanReferences(batchUpdate.update().traceId(), + batchUpdate.update().parentSpanId())) + .then(spanDAO.bulkUpdate(batchUpdate.ids(), batchUpdate.update(), mergeTags)) .onErrorResume(TagOperations::mapTagLimitError) .doOnSuccess(__ -> { log.info("Completed batch update for '{}' spans", batchUpdate.ids().size()); @@ -367,6 +375,15 @@ public Mono create(@NonNull SpanBatch batch) { List dedupedSpans = dedupSpans(batch.spans()); + // Fail fast on invalid ids BEFORE any side effect below (auto-stripped attachment deletion, project + // creation), so a rejected batch never mutates state. + dedupedSpans.forEach(span -> { + if (span.id() != null) { + idGenerator.validateId(span.id(), SPAN_KEY); + } + validateSpanReferences(span.traceId(), span.parentSpanId()); + }); + List projectNames = dedupedSpans .stream() .map(Span::projectName) @@ -438,6 +455,13 @@ private List dedupSpans(List initialSpans) { return result; } + // Shared span reference-id policy: the trace (required) and parent (optional) must be time-ordered + // UUIDv7, past allowed. Used by every span write path so the rules can't drift between them. + private void validateSpanReferences(UUID traceId, UUID parentSpanId) { + idGenerator.validateIdNotInFuture(traceId, SPAN_TRACE_KEY); + idGenerator.validateIdNotInFutureIfPresent(parentSpanId, SPAN_PARENT_KEY); + } + private List bindSpanToProjectAndId(List spans, List projects) { Map projectPerName = projects.stream() .collect(Collectors.toMap( @@ -460,6 +484,7 @@ private List bindSpanToProjectAndId(List spans, List projec UUID id = span.id() == null ? idGenerator.generateId() : span.id(); idGenerator.validateId(id, SPAN_KEY); + // trace/parent references are validated up front in create(SpanBatch) before side effects. return span.toBuilder().id(id).projectId(project.id()).build(); }) 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 1b3ddd9cc56..9cf964bc1cf 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 @@ -322,7 +322,7 @@ public Mono update(@NonNull TraceUpdate traceUpdate, @NonNull UUID id) { var projectName = WorkspaceUtils.getProjectName(traceUpdate.projectName()); return Mono.deferContextual(ctx -> idGenerator - .validateIdForUpdateAsync(id, TRACE_KEY) + .validateIdNotInFutureAsync(id, TRACE_KEY) .then(getProjectById(traceUpdate) .switchIfEmpty(Mono.defer(() -> projectService.getOrCreate(projectName))) .subscribeOn(Schedulers.boundedElastic()) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/attachment/AttachmentService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/attachment/AttachmentService.java index 989a941d05d..2504374c689 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/attachment/AttachmentService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/attachment/AttachmentService.java @@ -9,6 +9,7 @@ import com.comet.opik.api.attachment.EntityType; import com.comet.opik.api.attachment.StartMultipartUploadRequest; import com.comet.opik.api.attachment.StartMultipartUploadResponse; +import com.comet.opik.domain.IdGenerator; import com.comet.opik.domain.ProjectService; import com.comet.opik.infrastructure.OpikConfiguration; import com.comet.opik.infrastructure.auth.RequestContext; @@ -119,12 +120,14 @@ class AttachmentServiceImpl implements AttachmentService { private final @NonNull ProjectService projectService; private final @NonNull OpikConfiguration config; private final @NonNull Provider requestContext; + private final @NonNull IdGenerator idGenerator; private static final Tika tika = new Tika(); private static final int MAX_ATTACHMENTS_PER_ENTITY = 1_000; @Override public StartMultipartUploadResponse startMultiPartUpload(@NonNull StartMultipartUploadRequest startUploadRequest, @NonNull String workspaceId, @NonNull String userName) { + idGenerator.validateIdNotInFuture(startUploadRequest.entityId(), startUploadRequest.entityType().getValue()); if (config.getS3Config().isMinIO()) { return prepareMinIOUploadResponse(startUploadRequest); } @@ -149,6 +152,8 @@ public StartMultipartUploadResponse startMultiPartUpload(@NonNull StartMultipart public void completeMultiPartUpload(@NonNull CompleteMultipartUploadRequest completeUploadRequest, @NonNull String workspaceId, @NonNull String userName) { + idGenerator.validateIdNotInFuture(completeUploadRequest.entityId(), + completeUploadRequest.entityType().getValue()); // In case of MinIO complete is not needed, file is uploaded directly via BE if (config.getS3Config().isMinIO()) { log.info("Skipping completeMultiPartUpload for MinIO"); @@ -190,6 +195,7 @@ public void uploadAttachment(@NonNull AttachmentInfo attachmentInfo, byte[] data @Override public void uploadAttachmentInternal(@NonNull AttachmentInfo attachmentInfo, byte[] data, @NonNull String workspaceId, @NonNull String userName) { + idGenerator.validateIdNotInFuture(attachmentInfo.entityId(), attachmentInfo.entityType().getValue()); attachmentInfo = attachmentInfo.toBuilder() .containerId(getProjectIdByName(attachmentInfo.projectName(), workspaceId, userName)) diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/evaluators/AutomationRuleEvaluatorService.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/evaluators/AutomationRuleEvaluatorService.java index c2315fdb740..587c21c4732 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/evaluators/AutomationRuleEvaluatorService.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/evaluators/AutomationRuleEvaluatorService.java @@ -118,6 +118,8 @@ public > T save(@No UUID id = idGenerator.generateId(); IdGenerator.validateVersion(id, "AutomationRuleEvaluator"); + // projectIds are persisted without an existence check, so enforce v7 to avoid storing orphan v4 ids. + projectIds.forEach(projectId -> idGenerator.validateIdNotInFutureIfPresent(projectId, "project")); // Dual-field sync: First projectId becomes the legacy project_id field UUID primaryProjectId = projectIds.isEmpty() ? null : projectIds.iterator().next(); @@ -232,6 +234,8 @@ public > T save(@No public void update(@NonNull UUID id, @NonNull Set projectIds, @NonNull String workspaceId, @NonNull String userName, @NonNull AutomationRuleEvaluatorUpdate evaluatorUpdate) { + projectIds.forEach(projectId -> idGenerator.validateIdNotInFutureIfPresent(projectId, "project")); + log.debug("Updating AutomationRuleEvaluator with id '{}' in projectIds '{}' and workspaceId '{}'", id, projectIds, workspaceId); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AlertResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AlertResourceTest.java index ae5321dcf4b..3e310eb5331 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AlertResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AlertResourceTest.java @@ -51,6 +51,8 @@ import com.comet.opik.api.sorting.SortableFields; import com.comet.opik.api.sorting.SortingField; import com.comet.opik.domain.GuardrailResult; +import com.comet.opik.domain.IdGenerator; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; import com.comet.opik.infrastructure.DatabaseAnalyticsFactory; @@ -169,6 +171,7 @@ class AlertResourceTest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); private AlertResourceClient alertResourceClient; private PromptResourceClient promptResourceClient; @@ -1785,7 +1788,7 @@ void whenGuardrailsAreTriggeredForTrace_thenWebhookIsCalledBasedOnProjectScope( // Create guardrails for the trace Guardrail guardrail = factory.manufacturePojo(Guardrail.class).toBuilder() .entityId(trace.id()) - .secondaryId(UUID.randomUUID()) + .secondaryId(idGenerator.generateId()) .projectName(projectName) .result(GuardrailResult.FAILED) .build(); @@ -1904,7 +1907,7 @@ void whenGuardrailScopedByProjectIdColumn__thenWebhookFiresOnlyForMatchingProjec Guardrail guardrailA = factory.manufacturePojo(Guardrail.class).toBuilder() .entityId(traceA.id()) - .secondaryId(UUID.randomUUID()) + .secondaryId(idGenerator.generateId()) .projectName(projectAName) .result(GuardrailResult.FAILED) .build(); @@ -1929,7 +1932,7 @@ void whenGuardrailScopedByProjectIdColumn__thenWebhookFiresOnlyForMatchingProjec Guardrail guardrailB = factory.manufacturePojo(Guardrail.class).toBuilder() .entityId(traceB.id()) - .secondaryId(UUID.randomUUID()) + .secondaryId(idGenerator.generateId()) .projectName(projectBName) .result(GuardrailResult.FAILED) .build(); @@ -2720,7 +2723,7 @@ void testGuardrailsTriggeredEvent(AlertType alertType) { List guardrails = IntStream.range(0, 2) .mapToObj(i -> factory.manufacturePojo(Guardrail.class).toBuilder() .entityId(trace.id()) - .secondaryId(UUID.randomUUID()) + .secondaryId(idGenerator.generateId()) .projectName(projectName) .projectId(projectId) .result(GuardrailResult.FAILED) @@ -2812,7 +2815,7 @@ void testGuardrailsTriggeredEventWithFallback() { return factory.manufacturePojo(Guardrail.class).toBuilder() .entityId(trace.id()) - .secondaryId(UUID.randomUUID()) + .secondaryId(idGenerator.generateId()) .projectName(projectName) .projectId(projectId) .result(GuardrailResult.FAILED) diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AnnotationQueuesResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AnnotationQueuesResourceTest.java index d62f1160bee..bd77f89f8f4 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AnnotationQueuesResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AnnotationQueuesResourceTest.java @@ -28,6 +28,8 @@ import com.comet.opik.api.sorting.Direction; import com.comet.opik.api.sorting.SortableFields; import com.comet.opik.api.sorting.SortingField; +import com.comet.opik.domain.IdGenerator; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; import com.comet.opik.infrastructure.auth.WorkspaceUserPermission; @@ -122,6 +124,7 @@ class AnnotationQueuesResourceTest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); private AnnotationQueue newAnnotationQueue() { return factory.manufacturePojo(AnnotationQueue.class) @@ -550,9 +553,9 @@ void addItemsToAnnotationQueue() { // Generate some item IDs to add var itemIds = Set.of( - UUID.randomUUID(), - UUID.randomUUID(), - UUID.randomUUID()); + idGenerator.generateId(), + idGenerator.generateId(), + idGenerator.generateId()); // When & Then annotationQueuesResourceClient.addItemsToAnnotationQueue( @@ -578,8 +581,8 @@ void removeItemsFromAnnotationQueue() { // Generate some item IDs to add first, then remove var itemIds = Set.of( - UUID.randomUUID(), - UUID.randomUUID()); + idGenerator.generateId(), + idGenerator.generateId()); // Add items first annotationQueuesResourceClient.addItemsToAnnotationQueue( @@ -598,8 +601,8 @@ void removeItemsFromAnnotationQueue() { @DisplayName("should return 404 when adding items to non-existent annotation queue") void addItemsToAnnotationQueueWhenQueueNotExistsShouldReturn404() { // Given - Non-existent queue ID - var nonExistentQueueId = UUID.randomUUID(); - var itemIds = Set.of(UUID.randomUUID()); + var nonExistentQueueId = idGenerator.generateId(); + var itemIds = Set.of(idGenerator.generateId()); // When & Then annotationQueuesResourceClient.addItemsToAnnotationQueue( diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java index a6a8a595b55..8befcd881b9 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/AutomationRuleEvaluatorsResourceTest.java @@ -426,7 +426,7 @@ void setUp() { @DisplayName("create evaluator definition: when api key is present, then return proper response") void createAutomationRuleEvaluator__whenSessionTokenIsPresent__thenReturnProperResponse( String sessionToken, boolean isAuthorized, String workspaceName) { - var projectId = UUID.randomUUID(); + var projectId = generator.generate(); var ruleEvaluator = factory.manufacturePojo(AutomationRuleEvaluatorLlmAsJudge.class).toBuilder() .projectIds(Set.of(projectId)) .build(); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetVersionResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetVersionResourceTest.java index 8badd737da7..567d70d4697 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetVersionResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetVersionResourceTest.java @@ -47,7 +47,9 @@ import com.comet.opik.api.sorting.SortingField; import com.comet.opik.domain.DatasetVersionDAO; import com.comet.opik.domain.DatasetVersionService; +import com.comet.opik.domain.IdGenerator; import com.comet.opik.domain.SpanEnrichmentOptions; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.domain.TraceEnrichmentOptions; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; @@ -2819,6 +2821,8 @@ void createFromTraces__whenRegularDataset__thenDataContainsAllEnrichedFields() { @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ExperimentDatasetVersionLinking { + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); + private Experiment getExperiment(UUID id) { return experimentResourceClient.getExperiment(id, API_KEY, TEST_WORKSPACE); } @@ -3059,7 +3063,7 @@ void createExperiment_whenInvalidVersionId_thenConflict() { var datasetId = createDataset(datasetName); createDatasetItems(datasetId, 1); - var nonExistentVersionId = UUID.randomUUID(); + var nonExistentVersionId = idGenerator.generateId(); // when - create experiment with non-existent version ID var experiment = experimentResourceClient.createPartialExperiment() diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromSpansTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromSpansTest.java index c8e0cd91fb6..b47f0a1a8d3 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromSpansTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromSpansTest.java @@ -368,7 +368,7 @@ void createDatasetItemsFromSpans__whenDatasetNotFound__thenReturn404() { UUID nonExistentDatasetId = UUID.randomUUID(); var request = CreateDatasetItemsFromSpansRequest.builder() - .spanIds(Set.of(UUID.randomUUID())) + .spanIds(Set.of(GENERATOR.generate())) .enrichmentOptions( SpanEnrichmentOptions.builder().build()) .build(); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromTracesTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromTracesTest.java index b175f09c400..ea304d19c4f 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromTracesTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/DatasetsResourceCreateFromTracesTest.java @@ -351,7 +351,7 @@ void createDatasetItemsFromTraces__whenDatasetNotFound__thenReturn404() { UUID nonExistentDatasetId = UUID.randomUUID(); var request = CreateDatasetItemsFromTracesRequest.builder() - .traceIds(Set.of(UUID.randomUUID())) + .traceIds(Set.of(GENERATOR.generate())) .enrichmentOptions( TraceEnrichmentOptions.builder().build()) .build(); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceFindProjectExperimentsTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceFindProjectExperimentsTest.java index 06ed3ef76a7..85ad50c9780 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceFindProjectExperimentsTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ExperimentsResourceFindProjectExperimentsTest.java @@ -34,7 +34,9 @@ import com.comet.opik.api.sorting.SortableFields; import com.comet.opik.api.sorting.SortingField; import com.comet.opik.domain.FeedbackScoreMapper; +import com.comet.opik.domain.IdGenerator; import com.comet.opik.domain.SpanType; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; import com.comet.opik.podam.PodamFactoryUtils; @@ -110,6 +112,7 @@ class ExperimentsResourceFindProjectExperimentsTest { private final TestDropwizardAppExtension APP = setup.APP; private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); private String baseURI; private ExperimentResourceClient experimentResourceClient; @@ -1120,7 +1123,7 @@ void findByOptimizationIdAndType(ExperimentType type) { var project = factory.manufacturePojo(Project.class); var projectId = projectResourceClient.createProject(project, apiKey, workspaceName); - UUID optimizationId = UUID.randomUUID(); + UUID optimizationId = idGenerator.generateId(); var experiments = experimentResourceClient.generateExperimentList() .stream() 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 63665745ac1..85c1b2efb2a 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 @@ -1495,7 +1495,7 @@ void findByOptimizationIdAndType(ExperimentType type) { var apiKey = UUID.randomUUID().toString(); mockTargetWorkspace(apiKey, workspaceName, workspaceId); - UUID optimizationId = UUID.randomUUID(); + UUID optimizationId = GENERATOR.generate(); var experiments = experimentResourceClient.generateExperimentList() .stream() @@ -4785,7 +4785,7 @@ void createWithoutOptionalFieldsAndGet(String name) { void createWithOptimizationIdTypeAndGet(ExperimentType type) { var expectedExperiment = experimentResourceClient.createPartialExperiment() .type(type) - .optimizationId(UUID.randomUUID()) + .optimizationId(GENERATOR.generate()) .build(); var expectedId = createAndAssert(expectedExperiment, API_KEY, TEST_WORKSPACE); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FindSpansResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FindSpansResourceTest.java index 0fa794be813..6ff91e47975 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FindSpansResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/FindSpansResourceTest.java @@ -1506,7 +1506,7 @@ void whenFilterTraceIdEqual__thenReturnSpansFiltered(String endpoint, SpanPageTe mockTargetWorkspace(apiKey, workspaceName, workspaceId); var projectName = generator.generate().toString(); - var traceId = UUID.randomUUID(); + var traceId = generator.generate(); // Create 5 spans with the same trace_id (these should be returned by the filter) var expectedSpanCount = 5; @@ -1528,7 +1528,7 @@ void whenFilterTraceIdEqual__thenReturnSpansFiltered(String endpoint, SpanPageTe .mapToObj(i -> podamFactory.manufacturePojo(Span.class).toBuilder() .projectId(null) .projectName(projectName) - .traceId(UUID.randomUUID()) + .traceId(generator.generate()) .name("other-span-" + i) .feedbackScores(null) .totalEstimatedCost(null) diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GetTracesByProjectResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GetTracesByProjectResourceTest.java index 9689eaa3fe2..8232813e977 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GetTracesByProjectResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GetTracesByProjectResourceTest.java @@ -53,6 +53,7 @@ import com.comet.opik.domain.GuardrailsMapper; import com.comet.opik.domain.IdGenerator; import com.comet.opik.domain.SpanType; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.domain.cost.CostService; import com.comet.opik.domain.filter.FilterQueryBuilder; import com.comet.opik.extensions.DropwizardAppExtensionProvider; @@ -117,7 +118,6 @@ import static com.comet.opik.api.resources.utils.ClickHouseContainerUtils.DATABASE_NAME; import static com.comet.opik.api.resources.utils.TestUtils.toURLEncodedQueryParam; import static com.comet.opik.api.resources.utils.traces.TraceAssertions.IGNORED_FIELDS_TRACES; -import static java.util.UUID.randomUUID; import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toMap; import static org.assertj.core.api.Assertions.assertThat; @@ -172,6 +172,7 @@ class GetTracesByProjectResourceTest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator testIdGenerator = TestIdGeneratorFactory.create(); private final FilterQueryBuilder filterQueryBuilder = new FilterQueryBuilder(); private String baseURI; @@ -3947,7 +3948,7 @@ void whenFilterGuardrails__thenReturnTracesFiltered(String endpoint, TracePageTe var guardrailsByTraceId = traces.stream() .collect(Collectors.toMap(Trace::id, trace -> guardrailsGenerator.generateGuardrailsForTrace( - trace.id(), randomUUID(), trace.projectName()))); + trace.id(), testIdGenerator.generateId(), trace.projectName()))); // set the first trace with failed guardrails guardrailsByTraceId.put(traces.getFirst().id(), guardrailsByTraceId.get(traces.getFirst().id()).stream() @@ -5163,7 +5164,8 @@ void getTracesByProject__whenExcludeParamIdDefined__thenReturnSpanExcludingField .toList(); List guardrailsByTraceId = traces.stream() - .map(trace -> guardrailsGenerator.generateGuardrailsForTrace(trace.id(), randomUUID(), + .map(trace -> guardrailsGenerator.generateGuardrailsForTrace(trace.id(), + testIdGenerator.generateId(), trace.projectName())) .flatMap(Collection::stream) .toList(); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GuardrailsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GuardrailsResourceTest.java index a655bc18bd5..1573cddb438 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GuardrailsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/GuardrailsResourceTest.java @@ -17,6 +17,8 @@ import com.comet.opik.api.resources.utils.resources.TraceResourceClient; import com.comet.opik.domain.GuardrailResult; import com.comet.opik.domain.GuardrailsMapper; +import com.comet.opik.domain.IdGenerator; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.domain.stats.StatsMapper; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; @@ -82,6 +84,7 @@ public class GuardrailsResourceTest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); private TraceResourceClient traceResourceClient; private GuardrailsResourceClient guardrailsResourceClient; @@ -121,7 +124,8 @@ void testCreateGuardrails_getTraceById() { .build(); var traceId = traceResourceClient.createTrace(trace, API_KEY, TEST_WORKSPACE); - var guardrails = guardrailsGenerator.generateGuardrailsForTrace(traceId, randomUUID(), trace.projectName()); + var guardrails = guardrailsGenerator.generateGuardrailsForTrace(traceId, idGenerator.generateId(), + trace.projectName()); guardrailsResourceClient.addBatch(guardrails, API_KEY, TEST_WORKSPACE); Trace actual = traceResourceClient.getById(traceId, TEST_WORKSPACE, API_KEY); @@ -153,9 +157,11 @@ void testCreateGuardrails_findTraces() { var guardrailsByTraceId = traces.stream() .collect(Collectors.toMap(Trace::id, trace -> Stream.concat( // mimic two separate guardrails validation groups - guardrailsGenerator.generateGuardrailsForTrace(trace.id(), randomUUID(), trace.projectName()) + guardrailsGenerator.generateGuardrailsForTrace(trace.id(), idGenerator.generateId(), + trace.projectName()) .stream(), - guardrailsGenerator.generateGuardrailsForTrace(trace.id(), randomUUID(), trace.projectName()) + guardrailsGenerator.generateGuardrailsForTrace(trace.id(), idGenerator.generateId(), + trace.projectName()) .stream()) .toList())); @@ -189,7 +195,7 @@ void getTraceStats_containsGuardrails() { var guardrailsByTraceId = traces.stream() .collect(Collectors.toMap(Trace::id, trace -> guardrailsGenerator.generateGuardrailsForTrace( - trace.id(), randomUUID(), trace.projectName()))); + trace.id(), idGenerator.generateId(), trace.projectName()))); guardrailsByTraceId.values() .forEach(guardrail -> guardrailsResourceClient.addBatch(guardrail, API_KEY, diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/MultiValueFeedbackScoresE2ETest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/MultiValueFeedbackScoresE2ETest.java index fd8ca6b5b68..fb82e48bb4d 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/MultiValueFeedbackScoresE2ETest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/MultiValueFeedbackScoresE2ETest.java @@ -47,6 +47,8 @@ import com.comet.opik.api.resources.utils.traces.TraceAssertions; import com.comet.opik.domain.EntityType; import com.comet.opik.domain.FeedbackScoreDAO; +import com.comet.opik.domain.IdGenerator; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; import com.comet.opik.infrastructure.auth.RequestContext; @@ -125,6 +127,7 @@ class MultiValueFeedbackScoresE2ETest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); private TraceResourceClient traceResourceClient; private SpanResourceClient spanResourceClient; @@ -290,8 +293,8 @@ void deleteTraceFeedbackScoreScopedBySourceQueueId() { .build(); var traceId = traceResourceClient.createTrace(trace, API_KEY1, TEST_WORKSPACE); - var queueIdA = randomUUID(); - var queueIdB = randomUUID(); + var queueIdA = idGenerator.generateId(); + var queueIdB = idGenerator.generateId(); // Score from queue A var score = factory.manufacturePojo(FeedbackScore.class).toBuilder() @@ -343,8 +346,8 @@ void sameAuthorTwoQueuesProducesAverage() { .build(); var traceId = traceResourceClient.createTrace(trace, API_KEY1, TEST_WORKSPACE); - var queueIdA = randomUUID(); - var queueIdB = randomUUID(); + var queueIdA = idGenerator.generateId(); + var queueIdB = idGenerator.generateId(); var scoreName = randomUUID().toString(); var scoreFromQueueA = factory.manufacturePojo(FeedbackScore.class).toBuilder() diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsResourceTest.java index 23484888cb8..00870805053 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsResourceTest.java @@ -44,6 +44,7 @@ import com.comet.opik.domain.IdGenerator; import com.comet.opik.domain.ProjectMetricsDAO; import com.comet.opik.domain.ProjectMetricsService; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; import com.comet.opik.infrastructure.DatabaseAnalyticsFactory; @@ -131,7 +132,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static java.util.Collections.singletonMap; -import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -188,6 +188,7 @@ class ProjectMetricsResourceTest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator testIdGenerator = TestIdGeneratorFactory.create(); private IdGenerator idGenerator; private String baseURI; @@ -424,7 +425,7 @@ void happyPathWithFilter(Function getFilter, List e // create guardrails for the first trace var guardrail = guardrailsGenerator.generateGuardrailsForTrace( - traces.getFirst().id(), randomUUID(), projectName).getFirst().toBuilder() + traces.getFirst().id(), testIdGenerator.generateId(), projectName).getFirst().toBuilder() .result(GuardrailResult.PASSED) .build(); @@ -650,7 +651,7 @@ void happyPathWithFilter(Function getFilter, List e // create guardrails for the first trace var guardrail = guardrailsGenerator.generateGuardrailsForTrace( - traceForFilter.id(), randomUUID(), projectName).getFirst().toBuilder() + traceForFilter.id(), testIdGenerator.generateId(), projectName).getFirst().toBuilder() .result(GuardrailResult.PASSED) .build(); @@ -1055,7 +1056,7 @@ void happyPathWithFilter(Function getFilter, List e // create guardrails for the first trace var guardrail = guardrailsGenerator.generateGuardrailsForTrace( - traceForFilter.id(), randomUUID(), projectName).getFirst().toBuilder() + traceForFilter.id(), testIdGenerator.generateId(), projectName).getFirst().toBuilder() .result(GuardrailResult.PASSED) .build(); @@ -1274,7 +1275,7 @@ void happyPathWithFilter(Function getFilter, List e // create guardrails for the first trace var guardrail = guardrailsGenerator.generateGuardrailsForTrace( - traceForFilter.id(), randomUUID(), projectName).getFirst().toBuilder() + traceForFilter.id(), testIdGenerator.generateId(), projectName).getFirst().toBuilder() .result(GuardrailResult.PASSED) .build(); @@ -1466,7 +1467,7 @@ void happyPathWithFilter(Function getFilter, List e // create guardrails for the first trace var guardrail = guardrailsGenerator.generateGuardrailsForTrace( - traceForFilter.getFirst().id(), randomUUID(), projectName).getFirst().toBuilder() + traceForFilter.getFirst().id(), testIdGenerator.generateId(), projectName).getFirst().toBuilder() .result(GuardrailResult.PASSED) .build(); @@ -1685,7 +1686,7 @@ private Long createTracesWithGuardrails(String projectName, Instant marker) { return traces.stream() .map(trace -> { List guardrails = guardrailsGenerator.generateGuardrailsForTrace(trace.id(), - randomUUID(), + testIdGenerator.generateId(), trace.projectName()); guardrailsResourceClient.addBatch(guardrails, API_KEY, WORKSPACE_NAME); return guardrails; @@ -1711,7 +1712,8 @@ private Pair, List> createTracesWithGuardrails(String projectN List guardrailCounts = traces.stream() .map(trace -> { - var guardrails = guardrailsGenerator.generateGuardrailsForTrace(trace.id(), randomUUID(), + var guardrails = guardrailsGenerator.generateGuardrailsForTrace(trace.id(), + testIdGenerator.generateId(), trace.projectName()); var guardrailsWithAtLeastOneFailed = IntStream.range(0, guardrails.size()) .mapToObj(i -> i == 0 @@ -3515,7 +3517,7 @@ void happyPathWithFilter(Function getFilter, List e traceResourceClient.feedbackScores(scores, API_KEY, WORKSPACE_NAME); var guardrail = guardrailsGenerator.generateGuardrailsForTrace( - traceForFilter.getFirst().id(), randomUUID(), projectName).getFirst().toBuilder() + traceForFilter.getFirst().id(), testIdGenerator.generateId(), projectName).getFirst().toBuilder() .result(GuardrailResult.PASSED) .build(); guardrailsResourceClient.addBatch(List.of(guardrail), API_KEY, WORKSPACE_NAME); @@ -3633,7 +3635,7 @@ void happyPathWithFilter(Function getFilter, List e traceResourceClient.feedbackScores(scores, API_KEY, WORKSPACE_NAME); var guardrail = guardrailsGenerator.generateGuardrailsForTrace( - traceForFilter.id(), randomUUID(), projectName).getFirst().toBuilder() + traceForFilter.id(), testIdGenerator.generateId(), projectName).getFirst().toBuilder() .result(GuardrailResult.PASSED) .build(); guardrailsResourceClient.addBatch(List.of(guardrail), API_KEY, WORKSPACE_NAME); diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsWithBreakdownResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsWithBreakdownResourceTest.java index 1d317207ec7..ca2c3baccb1 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsWithBreakdownResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/ProjectMetricsWithBreakdownResourceTest.java @@ -26,6 +26,7 @@ import com.comet.opik.domain.GuardrailResult; import com.comet.opik.domain.IdGenerator; import com.comet.opik.domain.SpanType; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.extensions.DropwizardAppExtensionProvider; import com.comet.opik.extensions.RegisterApp; import com.comet.opik.infrastructure.DatabaseAnalyticsFactory; @@ -116,6 +117,7 @@ class ProjectMetricsWithBreakdownResourceTest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator testIdGenerator = TestIdGeneratorFactory.create(); private IdGenerator idGenerator; private String baseURI; @@ -1412,7 +1414,7 @@ private void createTracesWithGuardrails(String projectName, Instant marker, Brea traceResourceClient.createTrace(trace, API_KEY, WORKSPACE_NAME); // Add guardrail (alternating between pass and fail) - var guardrails = guardrailsGenerator.generateGuardrailsForTrace(trace.id(), UUID.randomUUID(), + var guardrails = guardrailsGenerator.generateGuardrailsForTrace(trace.id(), testIdGenerator.generateId(), projectName); // Set result to FAILED for even indices, PASSED for odd var guardrailsWithResult = guardrails.stream() 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 1dee24a875e..729fee90372 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 @@ -48,7 +48,9 @@ import com.comet.opik.domain.FeedbackScoreDAO; import com.comet.opik.domain.GuardrailResult; import com.comet.opik.domain.GuardrailsMapper; +import com.comet.opik.domain.IdGenerator; import com.comet.opik.domain.ProjectService; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.domain.retention.RetentionUtils; import com.comet.opik.domain.workspaces.WorkspacesService; import com.comet.opik.extensions.DropwizardAppExtensionProvider; @@ -131,7 +133,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static java.util.UUID.randomUUID; import static java.util.stream.Collectors.averagingDouble; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toMap; @@ -184,6 +185,7 @@ class ProjectsResourceTest { } private final PodamFactory factory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); private String baseURI; private ClientSupport client; @@ -1857,7 +1859,7 @@ private Project buildProjectStats(Project project, String apiKey, String workspa var guardrailsByTraceId = traces.stream() .collect(Collectors.toMap(Trace::id, trace -> guardrailsGenerator.generateGuardrailsForTrace( - trace.id(), randomUUID(), trace.projectName()))); + trace.id(), idGenerator.generateId(), trace.projectName()))); guardrailsByTraceId.values().forEach(guardrail -> guardrailsResourceClient.addBatch( guardrail, apiKey, workspaceName)); 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 9453f03dcfb..f40fd5e28fb 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 @@ -344,6 +344,33 @@ static Stream invalidIdsForUpdate() { "UUID after window")); } + // Referenced ids (traceId, parentSpanId) use the not-in-future policy: non-v7 and future-dated are + // rejected, but past ids are allowed (spans are commonly attached to older traces), so unlike + // invalidIds() there is no TOO_OLD case here. Each argument sets exactly one referenced id to an + // invalid value on the span builder and pairs it with the expected validation message, so trace and + // parent cases share a single test body. + static Stream invalidReferencedIds() { + var future = Instant.now().plus(Duration.ofHours(25)).toEpochMilli(); + var expectedDetails = "id with timestamp '%s' must be in the allowed ingestion window of '%s' around now, reason '%s'"; + var expectedWindow = Duration.ofHours(24); + return Stream.of( + arguments( + (Function) builder -> builder.traceId(UUID.randomUUID()), + "Span trace id must be a version 7 UUID", + "traceId not v7"), + arguments( + (Function) builder -> builder + .traceId(generator.construct(future)), + expectedDetails.formatted( + Instant.ofEpochMilli(future), expectedWindow, Reason.TOO_FAR_FUTURE.getValue()), + "traceId after window"), + arguments( + (Function) builder -> builder + .parentSpanId(UUID.randomUUID()), + "Span parent id must be a version 7 UUID", + "parentSpanId not v7")); + } + @Nested @DisplayName("Spans existence probe") class SpansExistence { @@ -1800,6 +1827,40 @@ void createWithInvalidIdThrowsBadRequest(UUID id, String expectedDetails, String } } + @MethodSource("com.comet.opik.api.resources.v1.priv.SpansResourceTest#invalidReferencedIds") + @ParameterizedTest(name = "Create span with invalid referenced id throws bad request: {2}") + void createWithInvalidReferencedIdThrowsBadRequest( + Function spanCustomizer, String expectedDetails, String testName) { + var expectedEntity = new io.dropwizard.jersey.errors.ErrorMessage( + HttpStatus.SC_BAD_REQUEST, "Invalid UUID for id", expectedDetails); + var span = spanCustomizer.apply(podamFactory.manufacturePojo(Span.class).toBuilder()).build(); + try (var response = spanResourceClient.createSpan( + span, API_KEY, TEST_WORKSPACE, HttpStatus.SC_BAD_REQUEST)) { + var actualEntity = response.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); + assertThat(actualEntity).isEqualTo(expectedEntity); + } + } + + @Test + @DisplayName("Create span with an old (past) v7 traceId succeeds — late spans on old traces are valid") + void createWithOldTraceIdSucceeds() { + var old = Instant.now().minus(Duration.ofDays(30)).toEpochMilli(); + var oldTraceId = generator.construct(old); + var span = podamFactory.manufacturePojo(Span.class).toBuilder() + .traceId(oldTraceId) + .parentSpanId(null) + .build(); + + var id = spanResourceClient.createSpan(span, API_KEY, TEST_WORKSPACE); + + assertThat(id).isNotNull(); + + // Round-trip: the old traceId must be persisted verbatim (not rewritten) and parentSpanId stays null. + var retrievedSpan = spanResourceClient.getById(id, TEST_WORKSPACE, API_KEY); + assertThat(retrievedSpan.traceId()).isEqualTo(oldTraceId); + assertThat(retrievedSpan.parentSpanId()).isNull(); + } + @Test @DisplayName("when span is fetched with different truncate and strip_attachments flags, then response varies accordingly") void getByList__whenFetchedWithDifferentFlags__thenResponseVariesAccordingly() throws Exception { @@ -2292,6 +2353,21 @@ void batchCreateWithInvalidIdThrowsBadRequest(UUID id, String expectedDetails, S } } + @MethodSource("com.comet.opik.api.resources.v1.priv.SpansResourceTest#invalidReferencedIds") + @ParameterizedTest(name = "Batch create span with invalid referenced id throws bad request: {2}") + void batchCreateWithInvalidReferencedIdThrowsBadRequest( + Function spanCustomizer, String expectedDetails, String testName) { + var expectedEntity = new io.dropwizard.jersey.errors.ErrorMessage( + HttpStatus.SC_BAD_REQUEST, "Invalid UUID for id", expectedDetails); + var span = spanCustomizer.apply(podamFactory.manufacturePojo(Span.class).toBuilder()).build(); + try (var response = spanResourceClient.callBatchCreateSpans( + List.of(span), API_KEY, TEST_WORKSPACE)) { + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); + var actualEntity = response.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); + assertThat(actualEntity).isEqualTo(expectedEntity); + } + } + private long readClickHouseErrorCount(TransactionTemplateAsync templateAsync, int errorCode) { return templateAsync.nonTransaction(connection -> { var statement = connection.createStatement( diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/WorkspaceVersionResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/WorkspaceVersionResourceTest.java index 8a165bbe994..309d0b80586 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/WorkspaceVersionResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/WorkspaceVersionResourceTest.java @@ -24,6 +24,8 @@ import com.comet.opik.api.resources.utils.resources.PromptResourceClient; import com.comet.opik.api.resources.utils.resources.WorkspaceResourceClient; import com.comet.opik.domain.DemoData; +import com.comet.opik.domain.IdGenerator; +import com.comet.opik.domain.TestIdGeneratorFactory; import com.comet.opik.domain.workspaces.Workspace; import com.comet.opik.domain.workspaces.WorkspacesService; import com.comet.opik.extensions.DropwizardAppExtensionProvider; @@ -83,6 +85,7 @@ class WorkspaceVersionResourceTest { .build(); private final PodamFactory podamFactory = PodamFactoryUtils.newPodamFactory(); + private static final IdGenerator idGenerator = TestIdGeneratorFactory.create(); @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -526,7 +529,7 @@ void workspaceVersion__whenMultiProjectRule__returnsVersion1() { // Single-project rule does not trigger version_1 evaluatorClient.createEvaluator(podamFactory.manufacturePojo(AutomationRuleEvaluatorLlmAsJudge.class) .toBuilder() - .projectIds(Set.of(UUID.randomUUID())) + .projectIds(Set.of(idGenerator.generateId())) .build(), workspaceName, API_KEY); assertThat(workspaceClient.getWorkspaceVersion(API_KEY, workspaceName)).isEqualTo(V2_WORKSPACE_VERSION); @@ -534,7 +537,7 @@ void workspaceVersion__whenMultiProjectRule__returnsVersion1() { // Multi-project rule triggers version_1 evaluatorClient.createEvaluator(podamFactory.manufacturePojo(AutomationRuleEvaluatorLlmAsJudge.class) .toBuilder() - .projectIds(Set.of(UUID.randomUUID(), UUID.randomUUID())) + .projectIds(Set.of(idGenerator.generateId(), idGenerator.generateId())) .build(), workspaceName, API_KEY); assertThat(workspaceClient.getWorkspaceVersion(API_KEY, workspaceName)).isEqualTo(V1_WORKSPACE_VERSION); @@ -597,7 +600,7 @@ void workspaceVersion__whenAlertWithoutProject__returnsVersion1() { // Project-scoped alert (projectId column) does not trigger version_1 alertClient.createAlert( - AlertResourceTest.generateAlertForProject(podamFactory, UUID.randomUUID()), + AlertResourceTest.generateAlertForProject(podamFactory, idGenerator.generateId()), API_KEY, workspaceName, 201); assertThat(workspaceClient.getWorkspaceVersion(API_KEY, workspaceName)).isEqualTo(V2_WORKSPACE_VERSION);