From a7cbf9cfddfc454817b2875ca4eb7c03d67c08bc Mon Sep 17 00:00:00 2001 From: Bhargav Date: Thu, 6 Aug 2026 13:42:46 +0530 Subject: [PATCH] fix(traces): warn when no correlated logs found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The span "View Logs" button called loadCorrelation() without awaiting it and then passed correlationProps straight to navigateToCorrelatedLogs. When nothing was correlated that value was null, so the composable threw on correlationProps.logStreams and the user saw nothing happen. The tree-view "View Logs" handler had the same gap, silently returning instead. Await the lookup and only navigate when at least one log stream came back; otherwise toast. A failed lookup reports its own reason, while a lookup that succeeded with nothing correlated gets the plain "no correlated logs" wording — correlationFailed tells the two apart, since correlationError is set in both cases. Also tighten the correlation check to require a non-empty logStreams array, so an empty result no longer navigates to Logs with an empty query. The two sidebar specs asserting the old fallback to navigateToLogs were already failing on this branch; they now cover the toast instead. --- web/src/locales/languages/en-US.json | 1 + web/src/plugins/traces/TraceDetails.vue | 9 +++- .../traces/TraceDetailsSidebar.spec.ts | 51 ++++++++++++++++--- .../plugins/traces/TraceDetailsSidebar.vue | 30 +++++++++-- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/web/src/locales/languages/en-US.json b/web/src/locales/languages/en-US.json index 35c8785e13..39cc36cc06 100644 --- a/web/src/locales/languages/en-US.json +++ b/web/src/locales/languages/en-US.json @@ -7467,6 +7467,7 @@ "RedMetrics": "RED Metrics", "loadingStream": "Loading Stream...", "noLogsAvailableForService": "No Logs available for this service", + "noCorrelatedLogsFound": "No correlated logs found for this span", "noData": { "title": "Start sending traces to OpenObserve", "description": "No trace streams found in this organization yet. Instrument your services and traces will appear here automatically.", diff --git a/web/src/plugins/traces/TraceDetails.vue b/web/src/plugins/traces/TraceDetails.vue index a5c2527b87..48c9bbb51c 100644 --- a/web/src/plugins/traces/TraceDetails.vue +++ b/web/src/plugins/traces/TraceDetails.vue @@ -2707,8 +2707,15 @@ export default defineComponent({ updateSelectedSpan(spanId); const correlationData = searchObj.data.traceDetails.correlationProps; - if (correlationData) { + if (correlationData?.logStreams?.length) { navigateToCorrelatedLogs(correlationData); + } else { + // The sidebar owns the correlation lookup; when it produced nothing there + // is no log stream to open, so tell the user rather than doing nothing. + toast({ + variant: "warning", + message: t("traces.noCorrelatedLogsFound"), + }); } }; diff --git a/web/src/plugins/traces/TraceDetailsSidebar.spec.ts b/web/src/plugins/traces/TraceDetailsSidebar.spec.ts index d441045389..d1f094bed4 100644 --- a/web/src/plugins/traces/TraceDetailsSidebar.spec.ts +++ b/web/src/plugins/traces/TraceDetailsSidebar.spec.ts @@ -23,11 +23,17 @@ const { mockBuildQueryDetails, mockNavigateToLogs, mockNavigateToCorrelatedLogs, + mockToast, } = vi.hoisted(() => ({ mockLoadSemanticGroups: vi.fn().mockResolvedValue([]), mockBuildQueryDetails: vi.fn().mockReturnValue({}), mockNavigateToLogs: vi.fn(), mockNavigateToCorrelatedLogs: vi.fn(), + mockToast: vi.fn(), +})); + +vi.mock("@/lib/feedback/Toast/useToast", () => ({ + toast: mockToast, })); vi.mock("@/utils/traces/convertTraceData", () => ({ @@ -439,16 +445,24 @@ describe("TraceDetailsSidebar", async () => { }); }); - describe("when isEnterprise is true but correlationProps is null (fallback path)", () => { + describe("when isEnterprise is true but no correlation was found", () => { beforeEach(() => { config.isEnterprise = "true"; - // correlationProps is null — correlation data hasn't been loaded yet + // Remount with a streamName so loadCorrelation() runs the real lookup + // instead of bailing out on a missing stream. findRelatedTelemetry is + // mocked to resolve null, i.e. the lookup succeeds with nothing correlated. + viewLogsWrapper.unmount(); + viewLogsWrapper = mountSidebar({ + parentMode: "standalone", + streamName: "default", + }); viewLogsWrapper.vm.correlationProps = null; + mockToast.mockClear(); dispatchSpy = vi.spyOn(mockStore, "dispatch").mockResolvedValue(undefined); pushSpy = vi.spyOn(router, "push").mockResolvedValue(undefined); }); - it("should fall back to buildQueryDetails and navigateToLogs when correlationProps is null", async () => { + it("should toast a warning instead of navigating when nothing is correlated", async () => { const viewLogsBtn = viewLogsWrapper.find( '[data-test="trace-details-sidebar-header-toolbar-view-logs-btn"]', ); @@ -457,11 +471,16 @@ describe("TraceDetailsSidebar", async () => { await viewLogsBtn.trigger("click"); await flushPromises(); - expect(mockBuildQueryDetails).toHaveBeenCalledWith(mockSpan); - expect(mockNavigateToLogs).toHaveBeenCalled(); + expect(mockToast).toHaveBeenCalledWith( + expect.objectContaining({ + variant: "warning", + message: "No correlated logs found for this span", + }), + ); + expect(pushSpy).not.toHaveBeenCalled(); }); - it("should not call navigateToCorrelatedLogs when correlationProps is null", async () => { + it("should not call navigateToCorrelatedLogs when nothing is correlated", async () => { const viewLogsBtn = viewLogsWrapper.find( '[data-test="trace-details-sidebar-header-toolbar-view-logs-btn"]', ); @@ -470,6 +489,26 @@ describe("TraceDetailsSidebar", async () => { expect(mockNavigateToCorrelatedLogs).not.toHaveBeenCalled(); }); + + it("should toast the lookup failure reason when correlation could not be loaded", async () => { + // No streamName — loadCorrelation() bails out and records why. + viewLogsWrapper.unmount(); + viewLogsWrapper = mountSidebar({ parentMode: "standalone" }); + mockToast.mockClear(); + + const viewLogsBtn = viewLogsWrapper.find( + '[data-test="trace-details-sidebar-header-toolbar-view-logs-btn"]', + ); + await viewLogsBtn.trigger("click"); + await flushPromises(); + + expect(mockToast).toHaveBeenCalledWith( + expect.objectContaining({ + variant: "warning", + message: "Missing span or stream name", + }), + ); + }); }); describe("when isEnterprise is false (non-enterprise branch)", () => { diff --git a/web/src/plugins/traces/TraceDetailsSidebar.vue b/web/src/plugins/traces/TraceDetailsSidebar.vue index 9f76c66f28..6086ba5c26 100644 --- a/web/src/plugins/traces/TraceDetailsSidebar.vue +++ b/web/src/plugins/traces/TraceDetailsSidebar.vue @@ -831,7 +831,7 @@ import { toggleFullscreen as domToggleFullScreen } from "@/utils/dom"; import { defineComponent, onBeforeMount, ref, watch, type Ref, type PropType, inject } from "vue"; import { useStore } from "vuex"; import useTheme from "@/composables/useTheme"; -import { useI18nTyped } from "@/types/i18n"; +import { useI18nTyped, raw } from "@/types/i18n"; import { computed } from "vue"; import { formatTimeWithSuffix, convertTimeFromNsToUs, getImageURL } from "@/utils/zincutils"; import useTraces from "@/composables/useTraces"; @@ -1439,9 +1439,23 @@ export default defineComponent({ }, ); - const viewSpanLogs = () => { - if (config.isEnterprise === "true" && correlationProps.value) { - navigateToCorrelatedLogs(correlationProps.value); + const viewSpanLogs = async () => { + if (config.isEnterprise === "true") { + await loadCorrelation(); + if (correlationProps.value?.logStreams?.length) { + navigateToCorrelatedLogs(correlationProps.value); + } else { + // Nothing correlated to this span — say so instead of navigating to an + // empty Logs page. A failed lookup reports its own reason; a successful + // lookup that found nothing gets the plain "no correlated logs" wording. + toast({ + variant: "warning", + message: + correlationFailed.value && correlationError.value + ? raw(correlationError.value) + : t("traces.noCorrelatedLogsFound"), + }); + } } else { const queryDetails = buildQueryDetails(props.span); navigateToLogs(queryDetails); @@ -1539,6 +1553,9 @@ export default defineComponent({ // Correlation state const correlationLoading = ref(false); const correlationError = ref(null); + // True when the lookup itself failed (request error, missing span/stream) as + // opposed to succeeding with nothing correlated — the two need different wording. + const correlationFailed = ref(false); const correlationProps = ref(null); const { findRelatedTelemetry, loadSemanticGroups, semanticGroups } = useServiceCorrelation(); @@ -1635,17 +1652,20 @@ export default defineComponent({ // Gate correlation feature behind enterprise check to avoid 403 errors if (config.isEnterprise !== "true") { correlationError.value = t("traces.traceDetailsSidebar.enterpriseLicenseRequired"); + correlationFailed.value = true; return; } if (!props.span || !props.streamName) { console.warn("[TraceDetailsSidebar] Cannot load correlation: missing span or stream name"); correlationError.value = t("traces.traceDetailsSidebar.missingSpanOrStream"); + correlationFailed.value = true; return; } correlationLoading.value = true; correlationError.value = null; + correlationFailed.value = false; try { try { @@ -1758,6 +1778,7 @@ export default defineComponent({ } catch (err: any) { console.error("[TraceDetailsSidebar] Correlation failed:", err); correlationError.value = err.message || t("correlation.failedToLoad"); + correlationFailed.value = true; } finally { correlationLoading.value = false; } @@ -1769,6 +1790,7 @@ export default defineComponent({ () => { correlationProps.value = null; correlationError.value = null; + correlationFailed.value = false; // Load correlation proactively so View Logs button has data if (props.serviceStreamsEnabled) {