Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
879ef9c8f9 | ||
|
|
a343d5b187 | ||
|
|
ac2b9eaa56 | ||
|
|
43e9cba99a | ||
|
|
601f431f69 | ||
|
|
4fd1f86f9d | ||
|
|
8245888e3c |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.97.3",
|
||||
"version": "3.97.4",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -88,7 +88,7 @@ async function removeIngestionEventsFromS3AndDeleteClickhouseRefs(p: {
|
||||
);
|
||||
await softDeleteInClickhouse(blobStorageRefs);
|
||||
logger.info(
|
||||
`Deleted batch ${batch} of size ${blobStorageRefs.length} for ${projectId} of deleting s3 refs`,
|
||||
`Deleted last batch ${batch} of size ${blobStorageRefs.length} for ${projectId} of deleting s3 refs`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { env } from "../../env";
|
||||
import { logger } from "../../server/logger";
|
||||
import {
|
||||
DatasetRunItemsExecutionStrategy,
|
||||
DatasetRunItemsOperationType,
|
||||
@@ -48,23 +47,8 @@ export async function executeWithDatasetRunItemsStrategy<TInput, TOutput>({
|
||||
if (operationType === DatasetRunItemsOperationType.WRITE) {
|
||||
// For write operations, implement dual-write strategy
|
||||
if (strategy.shouldWriteToClickHouse) {
|
||||
// Dual-write phase: write to both databases
|
||||
const postgresResult = await postgresExecution(input);
|
||||
|
||||
try {
|
||||
await clickhouseExecution(input);
|
||||
logger.debug("Successfully wrote to both PostgreSQL and ClickHouse", {
|
||||
operation: `dataset_run_items_${operationType}`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("ClickHouse write failed during dual-write phase", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
operation: `dataset_run_items_${operationType}`,
|
||||
});
|
||||
// Continue with PostgreSQL result since it succeeded
|
||||
}
|
||||
|
||||
return postgresResult;
|
||||
// Write only to ClickHouse
|
||||
return await clickhouseExecution(input);
|
||||
} else {
|
||||
// Write only to PostgreSQL
|
||||
return await postgresExecution(input);
|
||||
@@ -74,20 +58,10 @@ export async function executeWithDatasetRunItemsStrategy<TInput, TOutput>({
|
||||
const shouldExecuteClickhouse = strategy.shouldReadFromClickHouse;
|
||||
|
||||
if (shouldExecuteClickhouse) {
|
||||
try {
|
||||
return await clickhouseExecution(input);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"ClickHouse execution failed, falling back to PostgreSQL",
|
||||
{
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
operation: `dataset_run_items_${operationType}`,
|
||||
},
|
||||
);
|
||||
// Fallback to PostgreSQL for reliability
|
||||
return await postgresExecution(input);
|
||||
}
|
||||
// Read from ClickHouse
|
||||
return await clickhouseExecution(input);
|
||||
} else {
|
||||
// Read from PostgreSQL
|
||||
return await postgresExecution(input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const testModelCall = async ({
|
||||
apiKey: z.infer<typeof LLMApiKeySchema>;
|
||||
prompt?: string;
|
||||
modelConfig?: ModelConfig | null;
|
||||
}): Promise<void> => {
|
||||
}) => {
|
||||
(
|
||||
await fetchLLMCompletion({
|
||||
streaming: false,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.97.3",
|
||||
"version": "3.97.4",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.97.3";
|
||||
export const VERSION = "v3.97.4";
|
||||
|
||||
@@ -14,6 +14,19 @@ const isUniqueConstraintError = (error: any): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create or fetch a dataset run with optimistic concurrency handling.
|
||||
*
|
||||
* Behavior:
|
||||
* - First tries to find an existing run by (projectId, datasetId, name).
|
||||
* - If not found, attempts to create it.
|
||||
* - If creation fails due to a unique constraint (likely created concurrently),
|
||||
* fetches and returns the existing run.
|
||||
* - If all steps fail, throws an error.
|
||||
*
|
||||
* Rationale: The public API can receive many POST requests almost simultaneously,
|
||||
* which is not concurrency-safe without this guard.
|
||||
*/
|
||||
export const createOrFetchDatasetRun = async ({
|
||||
projectId,
|
||||
datasetId,
|
||||
@@ -28,7 +41,21 @@ export const createOrFetchDatasetRun = async ({
|
||||
metadata?: Json | null;
|
||||
}) => {
|
||||
try {
|
||||
// Attempt optimistic creation
|
||||
// Attempt to fetch existing run
|
||||
const existingRun = await prisma.datasetRuns.findUnique({
|
||||
where: {
|
||||
datasetId_projectId_name: {
|
||||
datasetId,
|
||||
projectId,
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (existingRun) {
|
||||
return existingRun;
|
||||
}
|
||||
|
||||
// Attempt creation
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
|
||||
@@ -1,50 +1,79 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { X } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
type TagInputProps = React.ComponentPropsWithoutRef<
|
||||
typeof CommandPrimitive.Input
|
||||
> & {
|
||||
selectedTags: string[];
|
||||
setSelectedTags?: (tags: string[]) => void;
|
||||
allowTagRemoval?: boolean;
|
||||
};
|
||||
|
||||
export const TagInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
TagInputProps
|
||||
>(({ className, selectedTags, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-wrap items-center overflow-auto rounded-lg border px-2"
|
||||
cmdk-input-wrapper=""
|
||||
>
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 pt-2">
|
||||
{selectedTags.map((tag: string) => (
|
||||
<Button
|
||||
key={tag}
|
||||
variant="tertiary"
|
||||
size="icon-sm"
|
||||
disabled
|
||||
className="cursor-default"
|
||||
>
|
||||
{tag}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"placeholder:muted-foreground flex h-8 w-full rounded-md border-transparent bg-transparent px-1 text-sm outline-none focus:border-0 focus:border-none focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
allowTagRemoval = false,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const removeTag = (tagToRemove: string) => {
|
||||
if (setSelectedTags && allowTagRemoval) {
|
||||
setSelectedTags(selectedTags.filter((t) => t !== tagToRemove));
|
||||
capture("tag:remove_tag", {
|
||||
name: tagToRemove,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-wrap items-center overflow-auto rounded-lg border px-2"
|
||||
cmdk-input-wrapper=""
|
||||
>
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 pt-2">
|
||||
{selectedTags.map((tag: string) => (
|
||||
<Button
|
||||
key={tag}
|
||||
variant="tertiary"
|
||||
size="icon-sm"
|
||||
disabled={!allowTagRemoval}
|
||||
className={
|
||||
allowTagRemoval ? "cursor-pointer" : "cursor-default"
|
||||
}
|
||||
onClick={allowTagRemoval ? () => removeTag(tag) : undefined}
|
||||
>
|
||||
{tag}
|
||||
{allowTagRemoval && <X className="ml-1 h-3 w-3" />}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
autoFocus
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"placeholder:muted-foreground flex h-8 w-full rounded-md border-transparent bg-transparent px-1 text-sm outline-none focus:border-0 focus:border-none focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
autoFocus
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TagInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
+3
@@ -22,6 +22,7 @@ type TagManagerProps = {
|
||||
mutateTags: (value: string[]) => void;
|
||||
className?: string;
|
||||
isTableCell?: boolean;
|
||||
allowTagRemoval?: boolean;
|
||||
};
|
||||
|
||||
const TagManager = ({
|
||||
@@ -33,6 +34,7 @@ const TagManager = ({
|
||||
mutateTags,
|
||||
className,
|
||||
isTableCell = false,
|
||||
allowTagRemoval = true,
|
||||
}: TagManagerProps) => {
|
||||
const {
|
||||
selectedTags,
|
||||
@@ -114,6 +116,7 @@ const TagManager = ({
|
||||
onValueChange={setInputValue}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
allowTagRemoval={allowTagRemoval}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandGroup
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import TagManager from "@/src/features/tag/components/TagManager";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagPromptDetailsPopoverProps = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import TagManager from "@/src/features/tag/components/TagManager";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagPromptPopverProps = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import TagManager from "@/src/features/tag/components/TagManager";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagTraceDetailsPopoverProps = {
|
||||
@@ -85,6 +85,7 @@ export function TagTraceDetailsPopover({
|
||||
isLoading={isLoading}
|
||||
mutateTags={mutateTags}
|
||||
className={className}
|
||||
allowTagRemoval={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
import TagManager from "@/src/features/tag/components/TagManager";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
|
||||
type TagTracePopoverProps = {
|
||||
@@ -75,6 +75,7 @@ export function TagTracePopover({
|
||||
mutateTags={mutateTags}
|
||||
className={className}
|
||||
isTableCell
|
||||
allowTagRemoval={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "3.97.3",
|
||||
"version": "3.97.4",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -588,26 +588,7 @@ describe("create experiment job calls with langfuse server side tracing", async
|
||||
await createExperimentJobPostgres({ event: mockEvent });
|
||||
|
||||
// Verify callLLM was called with correct trace parameters
|
||||
expect(callLLM).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Array),
|
||||
expect.any(Object),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
environment: PROMPT_EXPERIMENT_ENVIRONMENT,
|
||||
traceName: expect.stringMatching(/^dataset-run-item-/),
|
||||
traceId: expect.any(String),
|
||||
projectId: mockEvent.projectId,
|
||||
authCheck: expect.objectContaining({
|
||||
validKey: true,
|
||||
scope: expect.objectContaining({
|
||||
projectId: mockEvent.projectId,
|
||||
accessLevel: "project",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(callLLM).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.97.3";
|
||||
export const VERSION = "v3.97.4";
|
||||
|
||||
@@ -56,22 +56,21 @@ export const handleDataRetentionProcessingJob = async (job: Job) => {
|
||||
);
|
||||
}
|
||||
|
||||
await removeIngestionEventsFromS3AndDeleteClickhouseRefsForProject(
|
||||
projectId,
|
||||
cutoffDate,
|
||||
);
|
||||
|
||||
// Delete ClickHouse (TTL / Delete Queries)
|
||||
logger.info(
|
||||
`[Data Retention] Deleting ClickHouse data older than ${retention} days for project ${projectId}`,
|
||||
`[Data Retention] Deleting ClickHouse and S3 data older than ${retention} days for project ${projectId}`,
|
||||
);
|
||||
await Promise.all([
|
||||
removeIngestionEventsFromS3AndDeleteClickhouseRefsForProject(
|
||||
projectId,
|
||||
cutoffDate,
|
||||
),
|
||||
deleteTracesOlderThanDays(projectId, cutoffDate),
|
||||
deleteObservationsOlderThanDays(projectId, cutoffDate),
|
||||
deleteScoresOlderThanDays(projectId, cutoffDate),
|
||||
]);
|
||||
logger.info(
|
||||
`[Data Retention] Deleted ClickHouse data older than ${retention} days for project ${projectId}`,
|
||||
`[Data Retention] Deleted ClickHouse and S3 data older than ${retention} days for project ${projectId}`,
|
||||
);
|
||||
|
||||
// Set S3 Lifecycle for deletion (Future)
|
||||
|
||||
@@ -262,6 +262,9 @@ const EnvSchema = z.object({
|
||||
.positive()
|
||||
.default(2),
|
||||
LANGFUSE_DELETE_BATCH_SIZE: z.coerce.number().positive().default(2000),
|
||||
LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_TRACE_SOURCE_CH: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
});
|
||||
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { kyselyPrisma, prisma } from "@langfuse/shared/src/db";
|
||||
import z from "zod/v4";
|
||||
import { createHash } from "crypto";
|
||||
import { env } from "../../env";
|
||||
|
||||
export enum TraceExecutionSource {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -52,7 +53,9 @@ export enum TraceExecutionSource {
|
||||
*
|
||||
*/
|
||||
export const shouldCreateTrace = (source: TraceExecutionSource) => {
|
||||
// TODO: use env variable instead
|
||||
if (env.LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_TRACE_SOURCE_CH === "true") {
|
||||
return source === TraceExecutionSource.CLICKHOUSE;
|
||||
}
|
||||
return source === TraceExecutionSource.POSTGRES;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user