From 14fbeb17886944f46093cfd2721c4d333fd7ad89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire?= Date: Tue, 2 Sep 2025 08:35:58 +0200 Subject: [PATCH 01/11] Map gen_ai.input/output.messages to input/output (#8813) --- .../server/api/otel/otelMapping.servertest.ts | 24 +++++++++++++++++++ .../otel/server/OtelIngestionProcessor.ts | 7 ++++++ 2 files changed, 31 insertions(+) diff --git a/web/src/__tests__/server/api/otel/otelMapping.servertest.ts b/web/src/__tests__/server/api/otel/otelMapping.servertest.ts index 486294467b64..615570582676 100644 --- a/web/src/__tests__/server/api/otel/otelMapping.servertest.ts +++ b/web/src/__tests__/server/api/otel/otelMapping.servertest.ts @@ -1993,6 +1993,30 @@ describe("OTel Resource Span Mapping", () => { entityAttributeValue: ["2", "3", "4"], }, ], + [ + "should map gen_ai.input.messages to input", + { + entity: "observation", + otelAttributeKey: "gen_ai.input.messages", + otelAttributeValue: { + stringValue: '{"foo": "bar"}', + }, + entityAttributeKey: "input", + entityAttributeValue: '{"foo": "bar"}', + }, + ], + [ + "should map gen_ai.output.messages to output", + { + entity: "observation", + otelAttributeKey: "gen_ai.output.messages", + otelAttributeValue: { + stringValue: '{"foo": "bar"}', + }, + entityAttributeKey: "output", + entityAttributeValue: '{"foo": "bar"}', + }, + ], ])( "Attributes: %s", async ( diff --git a/web/src/features/otel/server/OtelIngestionProcessor.ts b/web/src/features/otel/server/OtelIngestionProcessor.ts index ae47d9511cf6..a545ebac86d8 100644 --- a/web/src/features/otel/server/OtelIngestionProcessor.ts +++ b/web/src/features/otel/server/OtelIngestionProcessor.ts @@ -1076,6 +1076,13 @@ export class OtelIngestionProcessor { }; } + // OpenTelemetry (https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans) + input = attributes["gen_ai.input.messages"]; + output = attributes["gen_ai.output.messages"]; + if (input || output) { + return { input, output }; + } + return { input: null, output: null }; } From 702dd6ac275cf56fc7fbca109b3d1c727e9a2678 Mon Sep 17 00:00:00 2001 From: Marc Klingen Date: Tue, 2 Sep 2025 14:18:18 +0200 Subject: [PATCH 02/11] ci: check pr title for conventional commit (#8862) --- .github/workflows/pipeline.yml | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 93a9a99d8798..2734da37dda2 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -469,6 +469,41 @@ jobs: - name: Run e2e tests run: pnpm --filter=web run test:e2e:server + validate-pr-title: + runs-on: ubuntu-latest + needs: + - pre-job + if: needs.pre-job.outputs.should_skip != 'true' && github.event_name == 'pull_request' + permissions: + statuses: write + pull-requests: read + steps: + - name: Validate PR title follows conventional commits + uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Configure the types of commits allowed + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + # Allow scopes (optional) + requireScope: false + # Allow any scope (not just the ones listed above) + validateSingleCommit: false + ignoreLabels: | + bot + ignore-semantic-pull-request + all-ci-passed: # This allows us to have a branch protection rule for tests and deploys with matrix runs-on: ubuntu-latest @@ -482,6 +517,7 @@ jobs: test-docker-build, e2e-server-tests, tests-web-async, + validate-pr-title, ] if: always() steps: From 816abfb59eb404175f58dd4cf61f58fbeeb16830 Mon Sep 17 00:00:00 2001 From: Nimar Date: Tue, 2 Sep 2025 15:20:22 +0200 Subject: [PATCH 03/11] chore: move obsevation type setting override to mapper (#8714) * chore: move obsevation type setting override to mapper * simplify mapper * map nicer * simplify --- .../server/api/otel/otelMapping.servertest.ts | 22 ++- .../otel/server/ObservationTypeMapper.ts | 137 +++++++++++++++--- .../otel/server/OtelIngestionProcessor.ts | 37 +---- 3 files changed, 139 insertions(+), 57 deletions(-) diff --git a/web/src/__tests__/server/api/otel/otelMapping.servertest.ts b/web/src/__tests__/server/api/otel/otelMapping.servertest.ts index 615570582676..15442c7effcd 100644 --- a/web/src/__tests__/server/api/otel/otelMapping.servertest.ts +++ b/web/src/__tests__/server/api/otel/otelMapping.servertest.ts @@ -3876,14 +3876,30 @@ describe("OTel Resource Span Mapping", () => { expect(traceEvents.length).toBe(1); }); - it("should override the observation type if it is declared as 'span' but holds generation-like attributes", async () => { + it("should override the observation type if it is declared as 'span' but holds generation-like attributes for python-sdk <= 3.3.0", async () => { // Issue: https://github.com/langfuse/langfuse/issues/8682 const otelSpans = [ { - resource: { attributes: [] }, + resource: { + attributes: [ + { + key: "telemetry.sdk.language", + value: { stringValue: "python" }, + }, + ], + }, scopeSpans: [ { - scope: { name: "test-scope" }, + scope: { + name: "langfuse-sdk", + version: "3.3.0", + attributes: [ + { + key: "public_key", + value: { stringValue: "pk-lf-1234567890" }, + }, + ], + }, spans: [ { traceId: { diff --git a/web/src/features/otel/server/ObservationTypeMapper.ts b/web/src/features/otel/server/ObservationTypeMapper.ts index ef3b834d94c9..81f0e4e93437 100644 --- a/web/src/features/otel/server/ObservationTypeMapper.ts +++ b/web/src/features/otel/server/ObservationTypeMapper.ts @@ -6,15 +6,18 @@ type LangfuseObservationType = keyof typeof ObservationType; interface ObservationTypeMapper { readonly name: string; readonly priority: number; // Lower numbers = higher priority - canMap(attributes: Record): boolean; + canMap( + attributes: Record, + resourceAttributes?: Record, + scopeData?: Record, + ): boolean; mapToObservationType( attributes: Record, + resourceAttributes?: Record, + scopeData?: Record, ): LangfuseObservationType | null; } -/** - * Simple mapper for direct attribute key-value mappings. - */ class SimpleAttributeMapper implements ObservationTypeMapper { constructor( public readonly name: string, @@ -23,7 +26,11 @@ class SimpleAttributeMapper implements ObservationTypeMapper { private readonly mappings: Record, ) {} - canMap(attributes: Record): boolean { + canMap( + attributes: Record, + _resourceAttributes?: Record, + _scopeData?: Record, + ): boolean { return ( this.attributeKey in attributes && attributes[this.attributeKey] != null ); @@ -31,6 +38,8 @@ class SimpleAttributeMapper implements ObservationTypeMapper { mapToObservationType( attributes: Record, + _resourceAttributes?: Record, + _scopeData?: Record, ): LangfuseObservationType | null { const value = attributes[this.attributeKey] as string; const mappedType = this.mappings[value]; @@ -53,20 +62,32 @@ class CustomAttributeMapper implements ObservationTypeMapper { constructor( public readonly name: string, public readonly priority: number, - private readonly canMapFn: (attributes: Record) => boolean, + private readonly canMapFn: ( + attributes: Record, + resourceAttributes?: Record, + scopeData?: Record, + ) => boolean, private readonly mapFn: ( attributes: Record, + resourceAttributes?: Record, + scopeData?: Record, ) => LangfuseObservationType | null, ) {} - canMap(attributes: Record): boolean { - return this.canMapFn(attributes); + canMap( + attributes: Record, + resourceAttributes?: Record, + scopeData?: Record, + ): boolean { + return this.canMapFn(attributes, resourceAttributes, scopeData); } mapToObservationType( attributes: Record, + resourceAttributes?: Record, + scopeData?: Record, ): LangfuseObservationType | null { - const result = this.mapFn(attributes); + const result = this.mapFn(attributes, resourceAttributes, scopeData); if ( result && @@ -89,7 +110,76 @@ class CustomAttributeMapper implements ObservationTypeMapper { */ export class ObservationTypeMapperRegistry { private readonly mappers: ObservationTypeMapper[] = [ - new SimpleAttributeMapper("OpenInference", 1, "openinference.span.kind", { + // Priority 0: Python SDK <= 3.3.0 override + // If generation-like attributes are set even though observation type is span, override to 'generation' + // Issue: https://github.com/langfuse/langfuse/issues/8682 + // Affected SDK versions: Python SDK <= 3.3.0 + new CustomAttributeMapper( + "PythonSDKv330Override", + 0, // Priority + // canMap? + (attributes, resourceAttributes, scopeData) => { + return ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] === "span" && + scopeData?.name === "langfuse-sdk" && + resourceAttributes?.["telemetry.sdk.language"] === "python" + ); + }, + // map! + (attributes, resourceAttributes, scopeData) => { + // Check version <= 3.3.0 + const scopeVersion = scopeData?.version as string; + if (scopeVersion) { + const [major, minor] = scopeVersion.split(".").map(Number); + if (major > 3 || (major === 3 && minor > 3)) { + return null; + } + } + + // Check for generation-like attributes + const generationKeys = [ + LangfuseOtelSpanAttributes.OBSERVATION_MODEL, + LangfuseOtelSpanAttributes.OBSERVATION_COST_DETAILS, + LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS, + LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME, + LangfuseOtelSpanAttributes.OBSERVATION_MODEL_PARAMETERS, + LangfuseOtelSpanAttributes.OBSERVATION_PROMPT_NAME, + LangfuseOtelSpanAttributes.OBSERVATION_PROMPT_VERSION, + ]; + + const hasGenerationAttributes = Object.keys(attributes).some((key) => + generationKeys.includes(key as any), + ); + + if (hasGenerationAttributes) { + return "GENERATION"; + } + + return null; + }, + ), + + // Priority 1: maps langfuse.observation.type directly + new SimpleAttributeMapper( + "LangfuseObservationTypeDirectMapping", + 1, + LangfuseOtelSpanAttributes.OBSERVATION_TYPE, + { + span: "SPAN", + generation: "GENERATION", + embedding: "EMBEDDING", + agent: "AGENT", + tool: "TOOL", + chain: "CHAIN", + retriever: "RETRIEVER", + guardrail: "GUARDRAIL", + evaluator: "EVALUATOR", + }, + ), + + new SimpleAttributeMapper("OpenInference", 2, "openinference.span.kind", { + // Format: + // OpenInference Value: Langfuse ObservationType CHAIN: "CHAIN", RETRIEVER: "RETRIEVER", LLM: "GENERATION", @@ -102,9 +192,11 @@ export class ObservationTypeMapperRegistry { new SimpleAttributeMapper( "OTel_GenAI_Operation", - 2, + 3, "gen_ai.operation.name", { + // Format: + // GenAI Value: Langfuse ObservationType chat: "GENERATION", completion: "GENERATION", generate_content: "GENERATION", @@ -116,7 +208,9 @@ export class ObservationTypeMapperRegistry { }, ), - new SimpleAttributeMapper("Vercel_AI_SDK_Operation", 3, "operation.name", { + new SimpleAttributeMapper("Vercel_AI_SDK_Operation", 4, "operation.name", { + // Format: + // Vercel AI SDK Value: Langfuse ObservationType "ai.generateText": "GENERATION", "ai.generateText.doGenerate": "GENERATION", "ai.streamText": "GENERATION", @@ -134,8 +228,8 @@ export class ObservationTypeMapperRegistry { new CustomAttributeMapper( "ModelBased", - 4, - (attributes) => { + 5, + (attributes, _resourceAttributes, _scopeData) => { const modelKeys = [ LangfuseOtelSpanAttributes.OBSERVATION_MODEL, "gen_ai.request.model", @@ -160,18 +254,19 @@ export class ObservationTypeMapperRegistry { return this.sortedMappersCache; } - /** - * Maps span attributes to a Langfuse observation type. - * Returns null if no mapper can handle the attributes. - */ mapToObservationType( attributes: Record, + resourceAttributes?: Record, + scopeData?: Record, ): LangfuseObservationType | null { const sortedMappers = this.getSortedMappers(); - for (const mapper of sortedMappers) { - if (mapper.canMap(attributes)) { - const result = mapper.mapToObservationType(attributes); + if (mapper.canMap(attributes, resourceAttributes, scopeData)) { + const result = mapper.mapToObservationType( + attributes, + resourceAttributes, + scopeData, + ); if (result) { return result; } diff --git a/web/src/features/otel/server/OtelIngestionProcessor.ts b/web/src/features/otel/server/OtelIngestionProcessor.ts index a545ebac86d8..26dce0f4fb27 100644 --- a/web/src/features/otel/server/OtelIngestionProcessor.ts +++ b/web/src/features/otel/server/OtelIngestionProcessor.ts @@ -641,38 +641,9 @@ export class OtelIngestionProcessor { }), }; - let observationType = attributes[ - LangfuseOtelSpanAttributes.OBSERVATION_TYPE - ] as string; - - // If generation-like attributes are set even though observation type is span, override to 'generation' - // Issue: https://github.com/langfuse/langfuse/issues/8682 - // Affected SDK versions: Python SDK <= 3.3.0 - const hasGenerationAttributes = Object.keys(attributes).some((key) => { - const generationKeys: LangfuseOtelSpanAttributes[] = [ - LangfuseOtelSpanAttributes.OBSERVATION_MODEL, - LangfuseOtelSpanAttributes.OBSERVATION_COST_DETAILS, - LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS, - LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME, - LangfuseOtelSpanAttributes.OBSERVATION_MODEL_PARAMETERS, - LangfuseOtelSpanAttributes.OBSERVATION_PROMPT_NAME, - LangfuseOtelSpanAttributes.OBSERVATION_PROMPT_VERSION, - ]; - - return generationKeys.includes(key as any); - }); - - if (observationType === "span" && hasGenerationAttributes) { - observationType = "generation"; - } - - // If no explicit observation type, try mapping from various frameworks - if (!observationType) { - const mappedType = observationTypeMapper.mapToObservationType(attributes); - if (mappedType) { - observationType = mappedType.toLowerCase(); - } - } + const observationType = observationTypeMapper + .mapToObservationType(attributes, resourceAttributes, scopeSpan?.scope) + ?.toLowerCase(); const isKnownObservationType = observationType && @@ -680,7 +651,7 @@ export class OtelIngestionProcessor { const getIngestionEventType = (): string => { if (isKnownObservationType) { - return `${observationType.toLowerCase()}-create`; + return `${observationType}-create`; } return "span-create"; }; From cf15ee50666f97365d9289401e32f3a32c657018 Mon Sep 17 00:00:00 2001 From: Nimar Date: Tue, 2 Sep 2025 18:26:20 +0200 Subject: [PATCH 04/11] feat(single-trace): add copy button on value cell of json view (#8871) --- web/src/components/table/ValueCell.tsx | 51 +++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/web/src/components/table/ValueCell.tsx b/web/src/components/table/ValueCell.tsx index 6c4d71948f84..b85a5c6ed9e1 100644 --- a/web/src/components/table/ValueCell.tsx +++ b/web/src/components/table/ValueCell.tsx @@ -1,7 +1,10 @@ -import { memo } from "react"; +import { memo, useState } from "react"; import { type Row } from "@tanstack/react-table"; import { urlRegex } from "@langfuse/shared"; import { type JsonTableRow } from "@/src/components/table/utils/jsonExpansionUtils"; +import { copyTextToClipboard } from "@/src/utils/clipboard"; +import { Button } from "@/src/components/ui/button"; +import { Copy, Check } from "lucide-react"; const MAX_STRING_LENGTH_FOR_LINK_DETECTION = 1500; const MAX_CELL_DISPLAY_CHARS = 2000; @@ -137,6 +140,20 @@ function getTruncatedValue(value: string, maxChars: number): string { return truncated + "..."; } +function getCopyValue(value: unknown): string { + if (typeof value === "string") { + return value; // Return string without quotes + } + if (value === null) return "null"; + if (value === undefined) return "undefined"; + + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + export const ValueCell = memo( ({ row, @@ -150,6 +167,20 @@ export const ValueCell = memo( const { value, type } = row.original; const cellId = `${row.id}-value`; const isCellExpanded = expandedCells.has(cellId); + const [showCopySuccess, setShowCopySuccess] = useState(false); + + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation(); + const copyValue = getCopyValue(value); + + try { + await copyTextToClipboard(copyValue); + setShowCopySuccess(true); + setTimeout(() => setShowCopySuccess(false), 1500); + } catch (error) { + // Copy failed silently + } + }; const getDisplayValue = () => { switch (type) { @@ -245,7 +276,7 @@ export const ValueCell = memo( const { content, needsTruncation } = getDisplayValue(); return ( -
+
{content} {needsTruncation && !row.original.hasChildren && (
)} + + {/* Copy button - appears on hover */} +
); }, From 2d6e58136d034337d3954646e20f6b8405b4b763 Mon Sep 17 00:00:00 2001 From: Steffen Schmitz Date: Tue, 2 Sep 2025 18:41:40 +0200 Subject: [PATCH 05/11] fix: use leftUTF8 for input/output truncation on clickhouse (#8869) * chose: use leftUTF8 for input/output truncation on clickhouse * chore: skip truncation linebreak --- packages/shared/src/server/repositories/observations.ts | 2 +- packages/shared/src/server/repositories/traces.ts | 8 ++++---- packages/shared/src/server/utils/rendering.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/server/repositories/observations.ts b/packages/shared/src/server/repositories/observations.ts index abdde96dd26f..12d099bb3c37 100644 --- a/packages/shared/src/server/repositories/observations.ts +++ b/packages/shared/src/server/repositories/observations.ts @@ -432,7 +432,7 @@ const getObservationByIdInternal = async ({ level, status_message, version, - ${fetchWithInputOutput ? (renderingProps.truncated ? `left(input, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT}) as input, left(output, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT}) as output,` : "input, output,") : ""} + ${fetchWithInputOutput ? (renderingProps.truncated ? `leftUTF8(input, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT}) as input, leftUTF8(output, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT}) as output,` : "input, output,") : ""} provided_model_name, internal_model_id, model_parameters, diff --git a/packages/shared/src/server/repositories/traces.ts b/packages/shared/src/server/repositories/traces.ts index a84c0104b94d..8b160a821d21 100644 --- a/packages/shared/src/server/repositories/traces.ts +++ b/packages/shared/src/server/repositories/traces.ts @@ -736,8 +736,8 @@ export const getTraceById = async ({ public as public, bookmarked as bookmarked, tags, - ${renderingProps.truncated ? `left(input, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "input"} as input, - ${renderingProps.truncated ? `left(output, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "output"} as output, + ${renderingProps.truncated ? `leftUTF8(input, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "input"} as input, + ${renderingProps.truncated ? `leftUTF8(output, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "output"} as output, session_id as session_id, 0 as is_deleted, timestamp, @@ -772,8 +772,8 @@ export const getTraceById = async ({ argMaxMerge(public) as public, argMaxMerge(bookmarked) as bookmarked, groupUniqArrayArray(tags) as tags, - ${renderingProps.truncated ? `left(argMaxMerge(input), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(input)"} as input, - ${renderingProps.truncated ? `left(argMaxMerge(output), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(output)"} as output, + ${renderingProps.truncated ? `leftUTF8(argMaxMerge(input), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(input)"} as input, + ${renderingProps.truncated ? `leftUTF8(argMaxMerge(output), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(output)"} as output, anyLast(session_id) as session_id, 0 as is_deleted, min(start_time) as timestamp, diff --git a/packages/shared/src/server/utils/rendering.ts b/packages/shared/src/server/utils/rendering.ts index 038df11e5135..331b3a46878b 100644 --- a/packages/shared/src/server/utils/rendering.ts +++ b/packages/shared/src/server/utils/rendering.ts @@ -42,14 +42,14 @@ export const applyInputOutputRendering = ( io.length > env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT ) { result = - io.slice(0, env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT) + "\n...[truncated]"; + io.slice(0, env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT) + "...[truncated]"; } if ( renderingProps.truncated && io.length === env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT ) { - result = io + "\n...[truncated]"; + result = io + "...[truncated]"; } return renderingProps.shouldJsonParse From 1e3f345f5bb2989b8354e14eccaae9de1ec97496 Mon Sep 17 00:00:00 2001 From: Leo Weigand <5489276+leoweigand@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:37:22 +0200 Subject: [PATCH 06/11] fix(trace-detail): guard against zero latency in trace tree (#8874) fix(trace-detail): guard against zero latency --- web/src/components/trace/SpanItem.tsx | 134 +++++++++++++------------- 1 file changed, 68 insertions(+), 66 deletions(-) diff --git a/web/src/components/trace/SpanItem.tsx b/web/src/components/trace/SpanItem.tsx index 53ff53a1aaf2..ab226ae780fb 100644 --- a/web/src/components/trace/SpanItem.tsx +++ b/web/src/components/trace/SpanItem.tsx @@ -61,6 +61,17 @@ export const SpanItem: React.FC = ({ ? node.latency * 1000 : undefined; + const shouldRenderMetrics = + showMetrics && + Boolean( + node.inputUsage || + node.outputUsage || + node.totalUsage || + duration || + totalCost || + node.latency, + ); + return (
@@ -93,72 +104,63 @@ export const SpanItem: React.FC = ({
- {showMetrics && - (node.inputUsage || - node.outputUsage || - node.totalUsage || - duration || - totalCost || - node.latency) && ( -
- {duration || node.latency ? ( - 0 || node.type === "TRACE" - ? "Aggregated duration of all child observations" - : undefined - } - className={cn( - "text-xs text-muted-foreground", - parentTotalDuration && - colorCodeMetrics && - heatMapTextColor({ - max: parentTotalDuration, - value: - duration || (node.latency ? node.latency * 1000 : 0), - }), - )} - > - {formatIntervalSeconds( - (duration || (node.latency ? node.latency * 1000 : 0)) / - 1000, - )} - - ) : null} - {node.inputUsage || node.outputUsage || node.totalUsage ? ( - - {formatTokenCounts( - node.inputUsage, - node.outputUsage, - node.totalUsage, - )} - - ) : null} - {totalCost ? ( - 0 || node.type === "TRACE" - ? "Aggregated cost of all child observations" - : undefined - } - className={cn( - "text-xs text-muted-foreground", - parentTotalCost && - colorCodeMetrics && - heatMapTextColor({ - max: parentTotalCost, - value: totalCost, - }), - )} - > - {node.children.length > 0 || node.type === "TRACE" - ? "∑ " - : ""} - {usdFormatter(totalCost.toNumber())} - - ) : null} -
- )} + {shouldRenderMetrics && ( +
+ {duration || node.latency ? ( + 0 || node.type === "TRACE" + ? "Aggregated duration of all child observations" + : undefined + } + className={cn( + "text-xs text-muted-foreground", + parentTotalDuration && + colorCodeMetrics && + heatMapTextColor({ + max: parentTotalDuration, + value: + duration || (node.latency ? node.latency * 1000 : 0), + }), + )} + > + {formatIntervalSeconds( + (duration || (node.latency ? node.latency * 1000 : 0)) / 1000, + )} + + ) : null} + {node.inputUsage || node.outputUsage || node.totalUsage ? ( + + {formatTokenCounts( + node.inputUsage, + node.outputUsage, + node.totalUsage, + )} + + ) : null} + {totalCost ? ( + 0 || node.type === "TRACE" + ? "Aggregated cost of all child observations" + : undefined + } + className={cn( + "text-xs text-muted-foreground", + parentTotalCost && + colorCodeMetrics && + heatMapTextColor({ + max: parentTotalCost, + value: totalCost, + }), + )} + > + {node.children.length > 0 || node.type === "TRACE" ? "∑ " : ""} + {usdFormatter(totalCost.toNumber())} + + ) : null} +
+ )} {showScores && ((node.type === "TRACE" && From 426d4b77b6567aa88254899001b63f8877a3bbc4 Mon Sep 17 00:00:00 2001 From: Steffen Schmitz Date: Wed, 3 Sep 2025 10:21:23 +0200 Subject: [PATCH 07/11] chore: patch flaky trace repository test (#8870) --- .../async/repositories/trace-repository.servertest.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web/src/__tests__/async/repositories/trace-repository.servertest.ts b/web/src/__tests__/async/repositories/trace-repository.servertest.ts index 82b6dba99fa7..47fbda58c74c 100644 --- a/web/src/__tests__/async/repositories/trace-repository.servertest.ts +++ b/web/src/__tests__/async/repositories/trace-repository.servertest.ts @@ -127,9 +127,16 @@ describe("Clickhouse Traces Repository Test", () => { expect(result.input).toEqual(null); expect(result.output).toEqual(null); expect(result.metadata).toEqual(trace.metadata); - expect(result.createdAt).toEqual(new Date(trace.created_at)); - expect(result.updatedAt).toEqual(new Date(trace.updated_at)); + expect(result.createdAt.getTime()).toBeCloseTo( + new Date(trace.created_at).getTime(), + -2, // Up to 50ms precision + ); + expect(result.updatedAt.getTime()).toBeCloseTo( + new Date(trace.updated_at).getTime(), + -2, // Up to 50ms precision + ); }); + it("should retrieve traces by session ID", async () => { const sessionId = v4(); const trace1 = createTrace({ From 2eebca4e42e3ffa565d9737bb270026cb6e93d77 Mon Sep 17 00:00:00 2001 From: Steffen Schmitz Date: Wed, 3 Sep 2025 10:37:27 +0200 Subject: [PATCH 08/11] chore: bump dd-trace to 5.65.0 (#8882) --- packages/shared/package.json | 2 +- pnpm-lock.yaml | 176 +++++++++++++++++++++++------------ web/Dockerfile | 2 +- web/package.json | 2 +- worker/package.json | 2 +- 5 files changed, 120 insertions(+), 64 deletions(-) diff --git a/packages/shared/package.json b/packages/shared/package.json index f9b68e3b3cac..68a275ee6e35 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -78,7 +78,7 @@ "@types/bcryptjs": "^2.4.6", "bcryptjs": "^2.4.3", "bullmq": "^5.34.10", - "dd-trace": "^5.36.0", + "dd-trace": "^5.65.0", "decimal.js": "^10.4.3", "exponential-backoff": "^3.1.2", "https-proxy-agent": "^7.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae6b2f82aae8..6e5e13b63a69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,8 +201,8 @@ importers: specifier: ^5.34.10 version: 5.34.10 dd-trace: - specifier: ^5.36.0 - version: 5.36.0 + specifier: ^5.65.0 + version: 5.65.0 decimal.js: specifier: ^10.4.3 version: 10.4.3 @@ -583,8 +583,8 @@ importers: specifier: ^3.3.1 version: 3.6.0 dd-trace: - specifier: ^5.36.0 - version: 5.36.0 + specifier: ^5.65.0 + version: 5.65.0 decimal.js: specifier: ^10.4.3 version: 10.4.3 @@ -917,8 +917,8 @@ importers: specifier: ^5.6.0 version: 5.6.0 dd-trace: - specifier: ^5.36.0 - version: 5.36.0 + specifier: ^5.65.0 + version: 5.65.0 decimal.js: specifier: ^10.4.3 version: 10.4.3 @@ -1841,31 +1841,31 @@ packages: '@dabh/diagnostics@2.0.3': resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} - '@datadog/libdatadog@0.4.0': - resolution: {integrity: sha512-kGZfFVmQInzt6J4FFGrqMbrDvOxqwk3WqhAreS6n9b/De+iMVy/NMu3V7uKsY5zAvz+uQw0liDJm3ZDVH/MVVw==} + '@datadog/libdatadog@0.7.0': + resolution: {integrity: sha512-VVZLspzQcfEU47gmGCVoRkngn7RgFRR4CHjw4YaX8eWT+xz4Q4l6PvA45b7CMk9nlt3MNN5MtGdYttYMIpo6Sg==} - '@datadog/native-appsec@8.4.0': - resolution: {integrity: sha512-LC47AnpVLpQFEUOP/nIIs+i0wLb8XYO+et3ACaJlHa2YJM3asR4KZTqQjDQNy08PTAUbVvYWKwfSR1qVsU/BeA==} + '@datadog/native-appsec@10.1.0': + resolution: {integrity: sha512-IKV9L4MvQxrT6GK0k5n9oOWw34gsGaiHW/03J1DOEu1crUqXcSWYJVOrGnRwz6XPXf6LDtAvmR+AU1QwDcDsww==} engines: {node: '>=16'} - '@datadog/native-iast-rewriter@2.8.0': - resolution: {integrity: sha512-DKmtvlmCld9RIJwDcPKWNkKYWYQyiuOrOtynmBppJiUv/yfCOuZtsQV4Zepj40H33sLiQyi5ct6dbWl53vxqkA==} - engines: {node: '>= 10'} - - '@datadog/native-iast-taint-tracking@3.2.0': - resolution: {integrity: sha512-Mc6FzCoyvU5yXLMsMS9yKnEqJMWoImAukJXolNWCTm+JQYCMf2yMsJ8pBAm7KyZKliamM9rCn7h7Tr2H3lXwjA==} + '@datadog/native-iast-taint-tracking@4.0.0': + resolution: {integrity: sha512-2uF8RnQkJO5bmLi26Zkhxg+RFJn/uEsesYTflScI/Cz/BWv+792bxI+OaCKvhgmpLkm8EElenlpidcJyZm7GYw==} - '@datadog/native-metrics@3.1.0': - resolution: {integrity: sha512-yOBi4x0OQRaGNPZ2bx9TGvDIgEdQ8fkudLTFAe7gEM1nAlvFmbE5YfpH8WenEtTSEBwojSau06m2q7axtEEmCg==} + '@datadog/native-metrics@3.1.1': + resolution: {integrity: sha512-MU1gHrolwryrU4X9g+fylA1KPH3S46oqJPEtVyrO+3Kh29z80fegmtyrU22bNt8LigPUK/EdPCnSbMe88QbnxQ==} engines: {node: '>=16'} - '@datadog/pprof@5.5.1': - resolution: {integrity: sha512-3pZVYqc5YkZJOj9Rc8kQ/wG4qlygcnnwFU/w0QKX6dEdJh+1+dWniuUu+GSEjy/H0jc14yhdT2eJJf/F2AnHNw==} + '@datadog/pprof@5.9.0': + resolution: {integrity: sha512-7KretVkHUANWe31u9cGJpxmUkyrXsCD+fmlZQUz/zk9mtQNC4uBIKX53VUFfrVj/bxAhEEIPw5XTYiMc5RJLsw==} engines: {node: '>=16'} '@datadog/sketches-js@2.1.1': resolution: {integrity: sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg==} + '@datadog/wasm-js-rewriter@4.0.1': + resolution: {integrity: sha512-JRa05Je6gw+9+3yZnm/BroQZrEfNwRYCxms56WCCHzOBnoPihQLB0fWy5coVJS29kneCUueUvBvxGp6NVXgdqw==} + engines: {node: '>= 10'} + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -6948,12 +6948,12 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - dc-polyfill@0.1.6: - resolution: {integrity: sha512-UV33cugmCC49a5uWAApM+6Ev9ZdvIUMTrtCO9fj96TPGOQiea54oeO3tiEVdVeo3J9N2UdJEmbS4zOkkEA35uQ==} + dc-polyfill@0.1.10: + resolution: {integrity: sha512-9iSbB8XZ7aIrhUtWI5ulEOJ+IyUN+axquodHK+bZO4r7HfY/xwmo6I4fYYf+aiDom+WMcN/wnzCz+pKvHDDCug==} engines: {node: '>=12.17'} - dd-trace@5.36.0: - resolution: {integrity: sha512-okaqSKaDKLynUt9Jgpoj/0g7FXk5sFNHP5ZelB84g/vmQdDmpzZYXPo6HFY6ZbQT7NOHMZ/vNV46LZIjZc0Tvg==} + dd-trace@5.65.0: + resolution: {integrity: sha512-U4zt7n8hKxjA3y3GTbJI7+ix5iwO5agn+8p6MNIAPgq2JG49jB6hUf78HvrPjGWX5R0fBpyiceOl+aLCsZIHNg==} engines: {node: '>=18'} debounce@1.2.1: @@ -8263,6 +8263,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -8280,6 +8284,9 @@ packages: import-in-the-middle@1.13.0: resolution: {integrity: sha512-YG86SYDtrL/Yu8JgfWb7kjQ0myLeT1whw6fs/ZHFkXFcbk9zJU9lOCsSJHpvaPumU11nN3US7NW6x1YTk+HrUA==} + import-in-the-middle@1.14.2: + resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -8587,10 +8594,6 @@ packages: resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} engines: {node: ^18.17 || >=20.6.1} - istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} - engines: {node: '>=8'} - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -9543,6 +9546,9 @@ packages: module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -9578,6 +9584,9 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mutexify@1.4.0: + resolution: {integrity: sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -10439,6 +10448,10 @@ packages: resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==} engines: {node: '>=12.0.0'} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + protocols@2.0.2: resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} @@ -10501,6 +10514,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -10914,6 +10930,9 @@ packages: selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semifies@1.0.0: + resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -10988,8 +11007,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} shelljs.exec@1.1.8: resolution: {integrity: sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==} @@ -14002,27 +14022,22 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@datadog/libdatadog@0.4.0': {} + '@datadog/libdatadog@0.7.0': {} - '@datadog/native-appsec@8.4.0': + '@datadog/native-appsec@10.1.0': dependencies: node-gyp-build: 3.9.0 - '@datadog/native-iast-rewriter@2.8.0': - dependencies: - lru-cache: 7.18.3 - node-gyp-build: 4.8.2 - - '@datadog/native-iast-taint-tracking@3.2.0': + '@datadog/native-iast-taint-tracking@4.0.0': dependencies: node-gyp-build: 3.9.0 - '@datadog/native-metrics@3.1.0': + '@datadog/native-metrics@3.1.1': dependencies: node-addon-api: 6.1.0 node-gyp-build: 3.9.0 - '@datadog/pprof@5.5.1': + '@datadog/pprof@5.9.0': dependencies: delay: 5.0.0 node-gyp-build: 3.9.0 @@ -14032,6 +14047,13 @@ snapshots: '@datadog/sketches-js@2.1.1': {} + '@datadog/wasm-js-rewriter@4.0.1': + dependencies: + js-yaml: 4.1.0 + lru-cache: 7.18.3 + module-details-from-path: 1.0.4 + node-gyp-build: 4.8.2 + '@discoveryjs/json-ext@0.5.7': {} '@dnd-kit/accessibility@3.1.0(react@18.2.0)': @@ -20049,39 +20071,41 @@ snapshots: dayjs@1.11.13: {} - dc-polyfill@0.1.6: {} + dc-polyfill@0.1.10: {} - dd-trace@5.36.0: + dd-trace@5.65.0: dependencies: - '@datadog/libdatadog': 0.4.0 - '@datadog/native-appsec': 8.4.0 - '@datadog/native-iast-rewriter': 2.8.0 - '@datadog/native-iast-taint-tracking': 3.2.0 - '@datadog/native-metrics': 3.1.0 - '@datadog/pprof': 5.5.1 + '@datadog/libdatadog': 0.7.0 + '@datadog/native-appsec': 10.1.0 + '@datadog/native-iast-taint-tracking': 4.0.0 + '@datadog/native-metrics': 3.1.1 + '@datadog/pprof': 5.9.0 '@datadog/sketches-js': 2.1.1 + '@datadog/wasm-js-rewriter': 4.0.1 '@isaacs/ttlcache': 1.4.1 '@opentelemetry/api': 1.8.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.8.0) crypto-randomuuid: 1.0.0 - dc-polyfill: 0.1.6 - ignore: 5.3.2 - import-in-the-middle: 1.11.2 - istanbul-lib-coverage: 3.2.0 + dc-polyfill: 0.1.10 + ignore: 7.0.5 + import-in-the-middle: 1.14.2 + istanbul-lib-coverage: 3.2.2 jest-docblock: 29.7.0 + jsonpath-plus: 10.3.0 koalas: 1.0.2 limiter: 1.1.5 lodash.sortby: 4.7.0 - lru-cache: 7.18.3 - module-details-from-path: 1.0.3 + lru-cache: 10.4.3 + module-details-from-path: 1.0.4 + mutexify: 1.4.0 opentracing: 0.14.7 path-to-regexp: 0.1.12 pprof-format: 2.1.0 - protobufjs: 7.4.0 + protobufjs: 7.5.4 retry: 0.13.1 rfdc: 1.4.1 - semver: 7.7.1 - shell-quote: 1.8.1 + semifies: 1.0.0 + shell-quote: 1.8.3 source-map: 0.7.4 tlhunter-sorted-set: 0.1.0 ttl-set: 1.0.0 @@ -21703,6 +21727,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -21734,6 +21760,13 @@ snapshots: cjs-module-lexer: 1.4.1 module-details-from-path: 1.0.3 + import-in-the-middle@1.14.2: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.1 + module-details-from-path: 1.0.4 + import-local@3.1.0: dependencies: pkg-dir: 4.2.0 @@ -21996,8 +22029,6 @@ snapshots: lodash.isstring: 4.0.1 lodash.uniqby: 4.7.0 - istanbul-lib-coverage@3.2.0: {} - istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -23300,6 +23331,8 @@ snapshots: module-details-from-path@1.0.3: {} + module-details-from-path@1.0.4: {} + mrmime@2.0.1: {} ms@2.1.2: {} @@ -23351,6 +23384,10 @@ snapshots: mute-stream@2.0.0: {} + mutexify@1.4.0: + dependencies: + queue-tick: 1.0.1 + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -24186,6 +24223,21 @@ snapshots: '@types/node': 24.3.0 long: 5.2.3 + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.3.0 + long: 5.2.3 + protocols@2.0.2: {} proxy-addr@2.0.7: @@ -24273,6 +24325,8 @@ snapshots: queue-microtask@1.2.3: {} + queue-tick@1.0.1: {} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -24816,6 +24870,8 @@ snapshots: dependencies: parseley: 0.12.1 + semifies@1.0.0: {} + semver@5.7.2: {} semver@6.3.1: {} @@ -24895,7 +24951,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.1: {} + shell-quote@1.8.3: {} shelljs.exec@1.1.8: {} diff --git a/web/Dockerfile b/web/Dockerfile index 01e3733a74ea..d64a45fdcd18 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -121,7 +121,7 @@ RUN npm install -g --no-package-lock --no-save prisma@6.3.0 # Install dd-trace only if NEXT_PUBLIC_LANGFUSE_CLOUD_REGION is configured ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION RUN if [ -n "$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION" ]; then \ - npm install --no-package-lock --no-save dd-trace@5.36.0; \ + npm install --no-package-lock --no-save dd-trace@5.65.0; \ fi RUN MIGRATE_TARGET_ARCH=$(echo ${TARGETPLATFORM:-linux/amd64} | sed 's/\//-/g') && \ diff --git a/web/package.json b/web/package.json index bc207b96dad7..bac22e5116e7 100644 --- a/web/package.json +++ b/web/package.json @@ -106,7 +106,7 @@ "cors": "^2.8.5", "csv-parse": "^5.6.0", "date-fns": "^3.3.1", - "dd-trace": "^5.36.0", + "dd-trace": "^5.65.0", "decimal.js": "^10.4.3", "diff": "^7.0.0", "dompurify": "^3.2.4", diff --git a/worker/package.json b/worker/package.json index eb07d07084df..4ea97390e655 100644 --- a/worker/package.json +++ b/worker/package.json @@ -42,7 +42,7 @@ "bullmq": "^5.34.10", "cors": "^2.8.5", "csv-parse": "^5.6.0", - "dd-trace": "^5.36.0", + "dd-trace": "^5.65.0", "decimal.js": "^10.4.3", "dotenv": "^16.4.5", "exponential-backoff": "^3.1.2", From 5c8e4d641a6cbbf2eeca228b047d0cd55517ae35 Mon Sep 17 00:00:00 2001 From: Nimar Date: Wed, 3 Sep 2025 11:12:41 +0200 Subject: [PATCH 09/11] fix(prompts): subfolders hiding prompts (#8876) * fix(prompts): subfolders hiding prompts * Update web/src/features/prompts/server/routers/promptRouter.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix da lint! * fix --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .../async/prompts-trpc.servertest.ts | 186 ++++++++++++++++++ .../prompts/components/prompts-table.tsx | 109 +++++----- .../prompts/server/routers/promptRouter.ts | 90 +++++++-- 3 files changed, 302 insertions(+), 83 deletions(-) diff --git a/web/src/__tests__/async/prompts-trpc.servertest.ts b/web/src/__tests__/async/prompts-trpc.servertest.ts index 45fd1b5bac0d..d5b79de29eba 100644 --- a/web/src/__tests__/async/prompts-trpc.servertest.ts +++ b/web/src/__tests__/async/prompts-trpc.servertest.ts @@ -1013,4 +1013,190 @@ describe("prompts trpc", () => { expect(folderCountResult.totalCount).toBe(BigInt(2)); }); }); + + describe("folder navigation with conflicting names", () => { + it("should handle prompts where individual prompt name conflicts with folder prefix - BUG REPRODUCTION", async () => { + const { project, caller } = await prepare(); + + // Create the exact bug scenario: + // - a/prompt (individual prompt) + // - a/prompt/v1 (prompt in subfolder) + // - a/prompt/v2 (prompt in subfolder) + // Bug: LFE-6515 + await prisma.prompt.create({ + data: { + id: v4(), + projectId: project.id, + name: "a/prompt", + version: 1, + type: "text", + prompt: { text: "This is the individual a/prompt" }, + createdBy: "test-user", + tags: ["individual"], + }, + }); + + await prisma.prompt.create({ + data: { + id: v4(), + projectId: project.id, + name: "a/prompt/v1", + version: 1, + type: "text", + prompt: { text: "This is a/prompt/v1" }, + createdBy: "test-user", + tags: ["subfolder"], + }, + }); + + await prisma.prompt.create({ + data: { + id: v4(), + projectId: project.id, + name: "a/prompt/v2", + version: 1, + type: "text", + prompt: { text: "This is a/prompt/v2" }, + createdBy: "test-user", + tags: ["subfolder"], + }, + }); + + // Test 1: Root level should show folder "a" + const rootResults = await caller.prompts.all({ + projectId: project.id, + page: 0, + limit: 10, + filter: [], + orderBy: { column: "createdAt", order: "DESC" }, + }); + + // Should show exactly one item: the "a" folder + expect(rootResults.prompts).toHaveLength(1); + expect(rootResults.prompts[0].name).toBe("a"); + + // Test 2: Folder "a" level should show BOTH individual "prompt" AND folder entry for "prompt/*" + const folderAResults = await caller.prompts.all({ + projectId: project.id, + page: 0, + limit: 10, + filter: [], + orderBy: { column: "createdAt", order: "DESC" }, + pathPrefix: "a", + }); + + // We expect 2 items: individual "prompt" and folder "prompt" (for v1, v2) + expect(folderAResults.prompts).toHaveLength(2); + + // Should have both an individual prompt and a folder entry + const promptNames = folderAResults.prompts.map((p) => p.name); + expect(promptNames).toContain("prompt"); + + // Test 3: Folder "a/prompt" level should show v1 and v2 + const subfolderResults = await caller.prompts.all({ + projectId: project.id, + page: 0, + limit: 10, + filter: [], + orderBy: { column: "createdAt", order: "DESC" }, + pathPrefix: "a/prompt", + }); + + // Should show exactly 2 items: v1 and v2 + expect(subfolderResults.prompts).toHaveLength(2); + const subfolderNames = subfolderResults.prompts.map((p) => p.name); + expect(subfolderNames).toContain("v1"); + expect(subfolderNames).toContain("v2"); + + // Test 4: Count verification + const rootCount = await caller.prompts.count({ + projectId: project.id, + }); + expect(rootCount.totalCount).toBe(BigInt(1)); // Should show 1 folder + + const folderACount = await caller.prompts.count({ + projectId: project.id, + pathPrefix: "a", + }); + expect(folderACount.totalCount).toBe(BigInt(2)); // Should show individual + folder entry + + const subfolderCount = await caller.prompts.count({ + projectId: project.id, + pathPrefix: "a/prompt", + }); + expect(subfolderCount.totalCount).toBe(BigInt(2)); // Should show v1 and v2 + }); + + it("should maintain search functionality across conflicting folder/prompt names", async () => { + const { project, caller } = await prepare(); + + // Create searchable content in conflicting structure + await prisma.prompt.create({ + data: { + id: v4(), + projectId: project.id, + name: "search/test", + version: 1, + type: "text", + prompt: { text: "Individual search test with unique keyword" }, + createdBy: "test-user", + tags: ["searchable"], + }, + }); + + await prisma.prompt.create({ + data: { + id: v4(), + projectId: project.id, + name: "search/test/nested1", + version: 1, + type: "text", + prompt: { text: "Nested prompt with unique keyword" }, + createdBy: "test-user", + tags: ["nested"], + }, + }); + + await prisma.prompt.create({ + data: { + id: v4(), + projectId: project.id, + name: "search/test/nested2", + version: 1, + type: "text", + prompt: { text: "Another nested prompt" }, + createdBy: "test-user", + tags: ["nested"], + }, + }); + + // Test search at different folder levels + const searchResults = await caller.prompts.all({ + projectId: project.id, + page: 0, + limit: 10, + filter: [], + orderBy: { column: "createdAt", order: "DESC" }, + searchQuery: "unique", + searchType: ["content"], + }); + + // Should find prompts containing "unique" keyword + expect(searchResults.prompts.length).toBeGreaterThan(0); + + // Test search within specific folder + const folderSearchResults = await caller.prompts.all({ + projectId: project.id, + page: 0, + limit: 10, + filter: [], + orderBy: { column: "createdAt", order: "DESC" }, + pathPrefix: "search/test", + searchQuery: "nested", + searchType: ["content"], + }); + // Should find nested prompts within the folder + expect(folderSearchResults.prompts.length).toBeGreaterThan(0); + }); + }); }); diff --git a/web/src/features/prompts/components/prompts-table.tsx b/web/src/features/prompts/components/prompts-table.tsx index fc1b874cdcc2..178b2c6dc855 100644 --- a/web/src/features/prompts/components/prompts-table.tsx +++ b/web/src/features/prompts/components/prompts-table.tsx @@ -37,6 +37,7 @@ import { useFullTextSearch } from "@/src/components/table/use-cases/useFullTextS type PromptTableRow = { id: string; name: string; + fullPath: string; // used for navigation/API calls type: "folder" | "text" | "chat"; version?: number; createdAt?: Date; @@ -49,6 +50,7 @@ function createRow( data: Partial & { id: string; name: string; + fullPath: string; type: "folder" | "text" | "chat"; }, ): PromptTableRow { @@ -62,12 +64,6 @@ function createRow( }; } -function isFolder( - row: PromptTableRow, -): row is PromptTableRow & { type: "folder" } { - return row.type === "folder"; -} - function createBreadcrumbItems(currentFolderPath: string) { if (!currentFolderPath) return []; @@ -173,56 +169,38 @@ export function PromptTable() { })), ); - // Backend returns folder representatives, so we just need to detect them + const buildFullPath = (currentFolder: string, itemName: string) => + currentFolder ? `${currentFolder}/${itemName}` : itemName; + + // Backend returns folder representatives with row_type metadata const processedRowData = useMemo(() => { if (!promptsRowData.rows) return { ...promptsRowData, rows: [] }; const combinedRows: PromptTableRow[] = []; for (const prompt of promptsRowData.rows) { - const promptName = prompt.id; - - // Check if this prompt represents a folder - const isFolderRepresentative = currentFolderPath - ? promptName.includes("/") && - promptName.startsWith(`${currentFolderPath}/`) && - promptName.substring(currentFolderPath.length + 1).includes("/") - : promptName.includes("/"); - - if (isFolderRepresentative) { - // Convert folder representative to folder item - const folderPath = currentFolderPath - ? `${currentFolderPath}/${promptName.substring(currentFolderPath.length + 1).split("/")[0]}` - : promptName.split("/")[0]; - - const folderName = currentFolderPath - ? folderPath.substring(currentFolderPath.length + 1) - : folderPath; - - combinedRows.push( - createRow({ - id: folderPath, - name: folderName, - type: "folder", - }), - ); - } else { - // Regular prompt - combinedRows.push( - createRow({ - id: prompt.id, - name: currentFolderPath - ? prompt.id.substring(currentFolderPath.length + 1) - : prompt.id, - type: prompt.type as "text" | "chat", - version: prompt.version, - createdAt: prompt.createdAt, - labels: prompt.labels, - tags: prompt.tags, - numberOfObservations: Number(prompt.observationCount ?? 0), - }), - ); - } + const isFolder = (prompt as { row_type?: string }).row_type === "folder"; + const itemName = prompt.id; // id actually contains the name due to type mapping + const fullPath = buildFullPath(currentFolderPath, itemName); + const type = isFolder ? "folder" : (prompt.type as "text" | "chat"); + + combinedRows.push( + createRow({ + id: `${type}-${fullPath}`, // Unique ID for React keys + name: itemName, + fullPath, + type, + ...(isFolder + ? {} + : { + version: prompt.version, + createdAt: prompt.createdAt, + labels: prompt.labels, + tags: prompt.tags, + numberOfObservations: Number(prompt.observationCount ?? 0), + }), + }), + ); } return { @@ -272,7 +250,7 @@ export function PromptTable() { const name = row.getValue(); const rowData = row.row.original; - if (isFolder(rowData)) { + if (rowData.type === "folder") { return ( { setQueryParams({ - folder: rowData.id, // rowData.id contains the full folder path + folder: rowData.fullPath, pageIndex: 0, pageSize: queryParams.pageSize, }); @@ -298,9 +276,9 @@ export function PromptTable() { return name ? ( ) : undefined; }, @@ -311,7 +289,7 @@ export function PromptTable() { enableSorting: true, size: 70, cell: (row) => { - if (isFolder(row.row.original)) return null; + if (row.row.original.type === "folder") return null; return row.getValue(); }, }), @@ -330,7 +308,7 @@ export function PromptTable() { enableSorting: true, size: 200, cell: (row) => { - if (isFolder(row.row.original)) return null; + if (row.row.original.type === "folder") return null; const createdAt = row.getValue(); return createdAt ? : null; }, @@ -339,12 +317,12 @@ export function PromptTable() { header: "Number of Observations", size: 170, cell: (row) => { - if (isFolder(row.row.original)) return null; + if (row.row.original.type === "folder") return null; const numberOfObservations = row.getValue(); - const promptId = row.row.original.id; + const promptPath = row.row.original.fullPath; const filter = encodeURIComponent( - `promptName;stringOptions;;any of;${promptId}`, + `promptName;stringOptions;;any of;${promptPath}`, ); if (!promptMetrics.isSuccess) { return ; @@ -364,16 +342,16 @@ export function PromptTable() { size: 120, cell: (row) => { // height h-6 to ensure consistent row height for normal & folder rows - if (isFolder(row.row.original)) return
; + if (row.row.original.type === "folder") return
; const tags = row.getValue(); - const promptId = row.row.original.id; + const promptPath = row.row.original.fullPath; return ( { - if (isFolder(row.row.original)) return null; + if (row.row.original.type === "folder") return null; - const promptId = row.row.original.id; - return ; + const promptPath = row.row.original.fullPath; + return ; }, }), ] as LangfuseColumnDef[]; @@ -490,6 +468,7 @@ export function PromptTable() { data: processedRowData.rows?.map((item) => ({ id: item.id, name: item.name, + fullPath: item.fullPath, version: item.version, createdAt: item.createdAt, type: item.type, diff --git a/web/src/features/prompts/server/routers/promptRouter.ts b/web/src/features/prompts/server/routers/promptRouter.ts index 3b533eccd8e9..d2f06bf358be 100644 --- a/web/src/features/prompts/server/routers/promptRouter.ts +++ b/web/src/features/prompts/server/routers/promptRouter.ts @@ -134,7 +134,7 @@ export const promptRouter = createTRPCRouter({ const [prompts, promptCount] = await Promise.all([ // prompts - ctx.prisma.$queryRaw>( + ctx.prisma.$queryRaw>( generatePromptQuery( Prisma.sql` p.id, @@ -146,7 +146,8 @@ export const promptRouter = createTRPCRouter({ p.updated_at as "updatedAt", p.created_at as "createdAt", p.labels, - p.tags`, + p.tags, + p.row_type`, input.projectId, filterCondition, orderByCondition, @@ -160,7 +161,7 @@ export const promptRouter = createTRPCRouter({ // promptCount ctx.prisma.$queryRaw>( generatePromptQuery( - Prisma.sql` count(*) AS "totalCount"`, + Prisma.sql`count(*) AS "totalCount"`, input.projectId, filterCondition, Prisma.empty, @@ -1397,24 +1398,64 @@ const generatePromptQuery = ( if (prefix) { // When we're inside a folder, show individual prompts within that folder // and folder representatives for subfolders - const segmentExpr = Prisma.sql`SPLIT_PART(SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2), '/', 1)`; return Prisma.sql` WITH ${latestCTE}, - grouped AS ( + individual_prompts_in_folder AS ( + /* Individual prompts exactly at this folder level (no deeper slashes) */ SELECT - p.*, /* keep all columns */ - ROW_NUMBER() OVER (PARTITION BY ${segmentExpr} ORDER BY p.version DESC) AS rn, - CASE - WHEN SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2) LIKE '%/%' THEN 1 - ELSE 2 - END as sort_priority -- Folders first (1), individual prompts second (2) + p.id, + SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2) as name, -- Remove prefix, show relative name + p.version, + p.project_id, + p.prompt, + p.type, + p.updated_at, + p.created_at, + p.labels, + p.tags, + p.config, + p.created_by, + 2 as sort_priority, -- Individual prompts second + 'prompt'::text as row_type -- Mark as individual prompt FROM latest p + WHERE SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2) NOT LIKE '%/%' + AND SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2) != '' -- Exclude prompts that match prefix exactly + AND p.name != ${prefix} -- Additional safety check + ), + subfolder_representatives AS ( + /* Folder representatives for deeper nested prompts */ + SELECT + p.id, + SPLIT_PART(SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2), '/', 1) as name, -- First segment after prefix + p.version, + p.project_id, + p.prompt, + p.type, + p.updated_at, + p.created_at, + p.labels, + p.tags, + p.config, + p.created_by, + 1 as sort_priority, -- Folders first + 'folder'::text as row_type, -- Mark as folder representative + ROW_NUMBER() OVER (PARTITION BY SPLIT_PART(SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2), '/', 1) ORDER BY p.version DESC) AS rn + FROM latest p + WHERE SUBSTRING(p.name, CHAR_LENGTH(${prefix}) + 2) LIKE '%/%' + ), + combined AS ( + SELECT + id, name, version, project_id, prompt, type, updated_at, created_at, labels, tags, config, created_by, sort_priority, row_type + FROM individual_prompts_in_folder + UNION ALL + SELECT + id, name, version, project_id, prompt, type, updated_at, created_at, labels, tags, config, created_by, sort_priority, row_type + FROM subfolder_representatives WHERE rn = 1 ) SELECT ${select} - FROM grouped p - WHERE rn = 1 + FROM combined p ${orderAndLimit}; `; } else { @@ -1426,22 +1467,35 @@ const generatePromptQuery = ( WITH ${latestCTE}, individual_prompts AS ( /* Individual prompts without folders */ - SELECT p.* + SELECT p.*, 'prompt'::text as row_type FROM latest p WHERE p.name NOT LIKE '%/%' ), folder_representatives AS ( - /* One representative per folder */ - SELECT p.*, + /* One representative per folder - return folder name, not full prompt name */ + SELECT + p.id, + SPLIT_PART(p.name, '/', 1) as name, -- Return folder segment name instead of full name + p.version, + p.project_id, + p.prompt, + p.type, + p.updated_at, + p.created_at, + p.labels, + p.tags, + p.config, + p.created_by, + 'folder'::text as row_type, -- Mark as folder representative ROW_NUMBER() OVER (PARTITION BY SPLIT_PART(p.name, '/', 1) ORDER BY p.version DESC) AS rn FROM latest p WHERE p.name LIKE '%/%' ), combined AS ( - SELECT ${baseColumns}, 1 as sort_priority -- Folders first + SELECT ${baseColumns}, row_type, 1 as sort_priority -- Folders first FROM folder_representatives WHERE rn = 1 UNION ALL - SELECT ${baseColumns}, 2 as sort_priority -- Individual prompts second + SELECT ${baseColumns}, row_type, 2 as sort_priority -- Individual prompts second FROM individual_prompts ) SELECT From 94e8f0b592527ce00967f6f3cad8f0573202c007 Mon Sep 17 00:00:00 2001 From: Nimar Date: Wed, 3 Sep 2025 12:02:16 +0200 Subject: [PATCH 10/11] fix(trace-single-ui): fix unavailable ref (#8883) * fix(trace-single-ui): fix unavailable ref * really fix it --- web/src/components/trace/index.tsx | 6 ++++++ .../trace-graph-view/buildStepData.ts | 6 ------ .../components/TraceGraphCanvas.tsx | 20 ++++++++++++++----- .../components/TraceGraphView.tsx | 6 ++++++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/web/src/components/trace/index.tsx b/web/src/components/trace/index.tsx index dd917470b381..18250aced63a 100644 --- a/web/src/components/trace/index.tsx +++ b/web/src/components/trace/index.tsx @@ -184,6 +184,12 @@ export function Trace(props: { return false; } + // don't show graph UI at all for extremely large traces + const MAX_NODES_FOR_GRAPH_UI = 5000; + if (agentGraphData.length >= MAX_NODES_FOR_GRAPH_UI) { + return false; + } + // Check if there are observations that would be included in the graph (not SPAN, EVENT, or GENERATION) const hasGraphableObservations = agentGraphData.some((obs) => { return ( diff --git a/web/src/features/trace-graph-view/buildStepData.ts b/web/src/features/trace-graph-view/buildStepData.ts index dc8075490b0a..9930a5a28a41 100644 --- a/web/src/features/trace-graph-view/buildStepData.ts +++ b/web/src/features/trace-graph-view/buildStepData.ts @@ -5,8 +5,6 @@ import { LANGFUSE_END_NODE_NAME, } from "./types"; -const MAX_NODE_NUMBER_FOR_PERFORMANCE = 250; - function buildStepGroups( observations: AgentGraphDataResponse[], timestampCache: Map, @@ -208,10 +206,6 @@ function addLangfuseSystemNodes( export function buildStepData( agentGraphData: AgentGraphDataResponse[], ): AgentGraphDataResponse[] { - if (agentGraphData.length >= MAX_NODE_NUMBER_FOR_PERFORMANCE) { - return []; - } - // for now, we don't want to show SPAN/EVENTs in our agent graphs // TODO: move this filter to a separate function const filteredData = agentGraphData.filter( diff --git a/web/src/features/trace-graph-view/components/TraceGraphCanvas.tsx b/web/src/features/trace-graph-view/components/TraceGraphCanvas.tsx index 3ba7330e5bfa..a91bcf5f9c0a 100644 --- a/web/src/features/trace-graph-view/components/TraceGraphCanvas.tsx +++ b/web/src/features/trace-graph-view/components/TraceGraphCanvas.tsx @@ -15,10 +15,16 @@ type TraceGraphCanvasProps = { graph: GraphCanvasData; selectedNodeName: string | null; onCanvasNodeNameChange: (nodeName: string | null) => void; + disablePhysics?: boolean; }; export const TraceGraphCanvas: React.FC = (props) => { - const { graph: graphData, selectedNodeName, onCanvasNodeNameChange } = props; + const { + graph: graphData, + selectedNodeName, + onCanvasNodeNameChange, + disablePhysics = false, + } = props; const [isHovering, setIsHovering] = useState(false); const containerRef = useRef(null); @@ -153,9 +159,9 @@ export const TraceGraphCanvas: React.FC = (props) => { randomSeed: 1, }, physics: { - enabled: true, + enabled: !disablePhysics, stabilization: { - iterations: 500, + iterations: disablePhysics ? 0 : 500, }, }, interaction: { @@ -201,7 +207,7 @@ export const TraceGraphCanvas: React.FC = (props) => { chosen: false, }, }), - [], + [disablePhysics], ); const handleZoomIn = () => { @@ -223,9 +229,13 @@ export const TraceGraphCanvas: React.FC = (props) => { }; useEffect(() => { + if (!containerRef.current) { + return; + } + // Create the network const network = new Network( - containerRef?.current!, + containerRef.current, { ...graphData, nodes }, options, ); diff --git a/web/src/features/trace-graph-view/components/TraceGraphView.tsx b/web/src/features/trace-graph-view/components/TraceGraphView.tsx index 34e3cdd8fab8..fc8c3b194d20 100644 --- a/web/src/features/trace-graph-view/components/TraceGraphView.tsx +++ b/web/src/features/trace-graph-view/components/TraceGraphView.tsx @@ -9,6 +9,8 @@ import { transformLanggraphToGeneralized, } from "../buildGraphCanvasData"; +const MAX_NODE_NUMBER_FOR_PHYSICS = 500; + type TraceGraphViewProps = { agentGraphData: AgentGraphDataResponse[]; }; @@ -46,6 +48,9 @@ export const TraceGraphView: React.FC = ({ return buildGraphFromStepData(normalizedData); }, [normalizedData]); + const shouldDisablePhysics = + agentGraphData.length >= MAX_NODE_NUMBER_FOR_PHYSICS; + useEffect(() => { const nodeName = Object.keys(nodeToParentObservationMap).find( (nodeKey) => nodeToParentObservationMap[nodeKey] === currentObservationId, @@ -84,6 +89,7 @@ export const TraceGraphView: React.FC = ({ graph={graph} selectedNodeName={selectedNodeName} onCanvasNodeNameChange={onCanvasNodeNameChange} + disablePhysics={shouldDisablePhysics} />
); From d022a07263f1b4a13f9cb350ad49dfb9c7ef91a5 Mon Sep 17 00:00:00 2001 From: Nimar Date: Wed, 3 Sep 2025 12:02:56 +0200 Subject: [PATCH 11/11] chore: release v3.106.4 --- package.json | 2 +- web/package.json | 2 +- web/src/constants/VERSION.ts | 2 +- worker/package.json | 2 +- worker/src/constants/VERSION.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 17cfdb24a109..59427bc22607 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "langfuse", - "version": "3.106.3", + "version": "3.106.4", "author": "engineering@langfuse.com", "license": "MIT", "private": true, diff --git a/web/package.json b/web/package.json index bac22e5116e7..24632dc5408b 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "3.106.3", + "version": "3.106.4", "private": true, "license": "MIT", "engines": { diff --git a/web/src/constants/VERSION.ts b/web/src/constants/VERSION.ts index 7e8726d5a198..41a2c7706699 100644 --- a/web/src/constants/VERSION.ts +++ b/web/src/constants/VERSION.ts @@ -1 +1 @@ -export const VERSION = "v3.106.3"; +export const VERSION = "v3.106.4"; diff --git a/worker/package.json b/worker/package.json index 4ea97390e655..886cdb619117 100644 --- a/worker/package.json +++ b/worker/package.json @@ -1,6 +1,6 @@ { "name": "worker", - "version": "3.106.3", + "version": "3.106.4", "description": "", "license": "MIT", "private": true, diff --git a/worker/src/constants/VERSION.ts b/worker/src/constants/VERSION.ts index 7e8726d5a198..41a2c7706699 100644 --- a/worker/src/constants/VERSION.ts +++ b/worker/src/constants/VERSION.ts @@ -1 +1 @@ -export const VERSION = "v3.106.3"; +export const VERSION = "v3.106.4";