mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
fix(autoobserve): age Last checked while the coverage page stays open
The live coverage source reads the shared ticking clock, rounded to the minute, so Last checked and the pass age advance without new data.
This commit is contained in:
@@ -299,8 +299,8 @@ const uniqueNames = (rows: AutoObserveCoverageRow[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
const minutesSince = (passAt: number | null) =>
|
||||
passAt ? Math.max(0, Math.floor((Date.now() - passAt / 1000) / 60_000)) : null;
|
||||
const minutesSince = (passAt: number | null, now: number) =>
|
||||
passAt ? Math.max(0, Math.floor((now - passAt / 1000) / 60_000)) : null;
|
||||
|
||||
const toRow = (
|
||||
t: TranslateFn,
|
||||
@@ -365,12 +365,13 @@ export const toCoverageSource = (
|
||||
data: AutoObserveCoverage | undefined,
|
||||
state: SourceState,
|
||||
locale: string,
|
||||
now = Date.now(),
|
||||
): CoverageSource => {
|
||||
const base = { busy: false, failure: null, detail: raw(""), stale: null, ...state };
|
||||
if (!data)
|
||||
return { ...base, rows: [], totals: EMPTY_TOTALS, planned: new Set(), passMinutes: null };
|
||||
const names = uniqueNames(data.rows);
|
||||
const minutes = minutesSince(data.pass_at);
|
||||
const minutes = minutesSince(data.pass_at, now);
|
||||
const rows = data.rows.map((row, i) => toRow(t, row, names[i], minutes, locale));
|
||||
const planned = new Set(data.rows.flatMap((row, i) => (cannotWatch(row) ? [names[i]] : [])));
|
||||
return { ...base, rows, totals: totalsOf(data, rows), planned, passMinutes: minutes };
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { flushPromises, mount } from "@vue/test-utils";
|
||||
import { defineComponent, h, nextTick } from "vue";
|
||||
import i18n from "@/locales";
|
||||
import store from "@/test/unit/helpers/store";
|
||||
import autoobserve, { type AutoObserveCoverage } from "@/services/autoobserve";
|
||||
import { useLiveAutoObserve } from "./liveModel";
|
||||
|
||||
vi.mock("vue-router", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useRoute: () => ({ query: { live: "1" } }),
|
||||
}));
|
||||
|
||||
vi.mock("@/services/autoobserve", () => ({
|
||||
default: { coverage: vi.fn(), suggestions: vi.fn() },
|
||||
}));
|
||||
|
||||
const START = Date.UTC(2026, 10, 2, 10, 0);
|
||||
|
||||
const cell = (state: string) => ({ state, evidence: [], counts: {} });
|
||||
|
||||
const coverage = (): AutoObserveCoverage =>
|
||||
({
|
||||
view: "strict",
|
||||
pass_at: START * 1000,
|
||||
refreshed_at: null,
|
||||
quality: null,
|
||||
classes: [],
|
||||
rows: [
|
||||
{
|
||||
service_id: "api",
|
||||
service_name: "api",
|
||||
cluster: "prod",
|
||||
namespace: "shop",
|
||||
streams: [],
|
||||
cells: { errors: cell("covered"), latency: cell("none"), absence: cell("none") },
|
||||
},
|
||||
],
|
||||
}) as unknown as AutoObserveCoverage;
|
||||
|
||||
const Harness = defineComponent({
|
||||
setup() {
|
||||
const { source } = useLiveAutoObserve();
|
||||
return () => h("span", { "data-test": "age" }, String(source.value.rows[0]?.lastScanned ?? ""));
|
||||
},
|
||||
});
|
||||
|
||||
describe("useLiveAutoObserve", () => {
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("ages Last checked while the page stays open on the same data", async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
|
||||
vi.setSystemTime(START);
|
||||
vi.mocked(autoobserve.coverage).mockResolvedValue({ data: coverage() } as never);
|
||||
vi.mocked(autoobserve.suggestions).mockResolvedValue({
|
||||
data: { suggestions: [], total: 0, limit: 5, offset: 0 },
|
||||
} as never);
|
||||
const wrapper = mount(Harness, { global: { plugins: [i18n, store] } });
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toBe("just now");
|
||||
vi.advanceTimersByTime(2 * 60 * 60_000);
|
||||
await nextTick();
|
||||
expect(wrapper.text()).toBe("2 hours ago");
|
||||
expect(autoobserve.coverage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -27,6 +27,7 @@ import { useRoute } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useI18nTyped } from "@/types/i18n";
|
||||
import { useNow } from "@/composables/useNow";
|
||||
import {
|
||||
autoobserveCoverageQuery,
|
||||
autoobserveSuggestionsPageQuery,
|
||||
@@ -168,6 +169,7 @@ export const useLiveAutoObserve = () => {
|
||||
const live = useLiveMode();
|
||||
const { t } = useI18nTyped();
|
||||
const zone = useLiveZone();
|
||||
const now = useNow();
|
||||
const org = computed<string>(() => store.state.selectedOrganization?.identifier ?? "");
|
||||
const enabled = computed(() => live.value && !!org.value);
|
||||
const coverage = useQuery(() =>
|
||||
@@ -187,8 +189,16 @@ export const useLiveAutoObserve = () => {
|
||||
() => stateOf(suggestions, retry),
|
||||
() => org.value,
|
||||
);
|
||||
// Ages are whole minutes, so the rows rebuild once a minute, not on every clock tick.
|
||||
const minute = computed(() => Math.floor(now.value / 60_000));
|
||||
const source = computed<CoverageSource>(() =>
|
||||
toCoverageSource(t, coverage.data.value, coverageState.value, zone.value.locale),
|
||||
toCoverageSource(
|
||||
t,
|
||||
coverage.data.value,
|
||||
coverageState.value,
|
||||
zone.value.locale,
|
||||
minute.value * 60_000,
|
||||
),
|
||||
);
|
||||
const rail = computed<RailSource>(() => ({
|
||||
...railState.value,
|
||||
|
||||
Reference in New Issue
Block a user