fix(traces): warn when no correlated logs found

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.
This commit is contained in:
Bhargav
2026-08-06 13:42:46 +05:30
parent c9f68754df
commit a7cbf9cfdd
4 changed files with 80 additions and 11 deletions
+1
View File
@@ -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.",
+8 -1
View File
@@ -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"),
});
}
};
@@ -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)", () => {
+26 -4
View File
@@ -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<string | null>(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<any>(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) {