From 9500a8b0364abbee7b8b9d7c9b5fc8d5458418c1 Mon Sep 17 00:00:00 2001 From: udev Date: Wed, 4 Nov 2026 09:58:51 +0000 Subject: [PATCH] fix(ai-obs): let a deep link's date range win over the saved one The date picker seeds itself from the shared date state in its own onMounted, which runs before the page's, and replays that window into the URL. Resolve the URL window in setup on Insights, Sessions and Discovery so a link's from/to or period reaches the picker. --- .../views/AIObservability/DiscoveryPage.vue | 8 +- .../AIObservability/LLMInsightsPage.spec.ts | 107 ++++++++++++++++++ .../views/AIObservability/LLMInsightsPage.vue | 11 +- .../views/AIObservability/SessionsPage.vue | 9 +- 4 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 web/src/enterprise/views/AIObservability/LLMInsightsPage.spec.ts diff --git a/web/src/enterprise/views/AIObservability/DiscoveryPage.vue b/web/src/enterprise/views/AIObservability/DiscoveryPage.vue index d16c71b31a..e39d53f5df 100644 --- a/web/src/enterprise/views/AIObservability/DiscoveryPage.vue +++ b/web/src/enterprise/views/AIObservability/DiscoveryPage.vue @@ -756,8 +756,8 @@ async function addToQueue(queue: LlmQueue, rows: LlmDiscoveryItem[]) { } } -onMounted(() => { - mountResolve(); - fetchItems(); -}); +// Resolved in setup, not onMounted: the date picker seeds from dateState in its own +// onMounted, which runs first and would replay the stored window over a deep link. +mountResolve(); +onMounted(fetchItems); diff --git a/web/src/enterprise/views/AIObservability/LLMInsightsPage.spec.ts b/web/src/enterprise/views/AIObservability/LLMInsightsPage.spec.ts new file mode 100644 index 0000000000..68cd51a9b9 --- /dev/null +++ b/web/src/enterprise/views/AIObservability/LLMInsightsPage.spec.ts @@ -0,0 +1,107 @@ +// Copyright 2026 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// @vitest-environment jsdom + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { defineComponent, onMounted } from "vue"; +import { mount } from "@vue/test-utils"; +import type * as I18n from "@/types/i18n"; + +const route = { query: {} as Record }; +const replace = vi.fn((_to: { query: Record }) => Promise.resolve()); +vi.mock("vue-router", () => ({ useRoute: () => route, useRouter: () => ({ replace }) })); + +vi.mock("@/types/i18n", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, useI18nTyped: () => ({ t: (key: string) => key }) }; +}); + +// Like DateTime: the picker seeds itself from `dateState` in its own onMounted, which runs +// before the page's, and replays that window as a date-change. +const pickerSeed: { value: Record | null } = { value: null }; +vi.mock("@/enterprise/components/AIObservability/AiPageShell.vue", () => ({ + default: defineComponent({ + name: "AiPageShell", + props: ["dateState"], + emits: ["date-change"], + setup(props, { emit }) { + onMounted(() => { + pickerSeed.value = { ...props.dateState }; + emit("date-change", { ...props.dateState, userChangedValue: false }); + }); + }, + template: "
", + }), +})); + +vi.mock("@/plugins/traces/LLMInsightsDashboard.vue", () => ({ + default: { name: "LLMInsightsDashboard", template: "
" }, +})); + +vi.mock("@/enterprise/composables/useChildRefresh", () => ({ + useChildRefresh: () => ({ + lastRunAt: { value: null }, + isLoading: { value: false }, + refresh: vi.fn(), + }), +})); + +const SAVED = { + valueType: "absolute", + startTime: 1_000_000, + endTime: 2_000_000, + relativeTimePeriod: null, +}; + +async function mountPage(query: Record) { + route.query = query; + // The shared date state is a module singleton loaded from localStorage at import, so each + // test re-imports the page to load the range it saved. + vi.resetModules(); + const Page = (await import("./LLMInsightsPage.vue")).default; + return mount(Page); +} + +beforeEach(() => { + pickerSeed.value = null; + replace.mockClear(); + localStorage.setItem("aiObservability:dateRange", JSON.stringify(SAVED)); +}); + +describe("LLMInsightsPage deep link over a saved date range", () => { + it("seeds the picker and the URL with the link's from/to", async () => { + await mountPage({ from: "5000000", to: "9000000", stream: "agent_e2e" }); + expect(pickerSeed.value).toMatchObject({ + valueType: "absolute", + startTime: 5_000_000, + endTime: 9_000_000, + }); + for (const [arg] of replace.mock.calls) { + expect(arg.query).toMatchObject({ from: "5000000", to: "9000000", stream: "agent_e2e" }); + } + }); + + it("seeds the picker with the link's period", async () => { + await mountPage({ period: "6h" }); + expect(pickerSeed.value).toMatchObject({ valueType: "relative", relativeTimePeriod: "6h" }); + for (const [arg] of replace.mock.calls) expect(arg.query.period).toBe("6h"); + }); + + it("keeps the saved range when the link has no date", async () => { + await mountPage({ stream: "agent_e2e" }); + expect(pickerSeed.value).toMatchObject({ startTime: 1_000_000, endTime: 2_000_000 }); + }); +}); diff --git a/web/src/enterprise/views/AIObservability/LLMInsightsPage.vue b/web/src/enterprise/views/AIObservability/LLMInsightsPage.vue index e9b67e44f3..2d06900eef 100644 --- a/web/src/enterprise/views/AIObservability/LLMInsightsPage.vue +++ b/web/src/enterprise/views/AIObservability/LLMInsightsPage.vue @@ -39,7 +39,7 @@ along with this program. If not, see . diff --git a/web/src/enterprise/views/AIObservability/SessionsPage.vue b/web/src/enterprise/views/AIObservability/SessionsPage.vue index 0736808165..4c469152d9 100644 --- a/web/src/enterprise/views/AIObservability/SessionsPage.vue +++ b/web/src/enterprise/views/AIObservability/SessionsPage.vue @@ -38,7 +38,7 @@ along with this program. If not, see .