refactor(editor): rename useFieldValueStore, and write down what it captures

I claimed field values could only ever exist for logs streams, and repeated it
after being questioned. Wrong twice over: the sidebar path captureFromValuesApi
is reached from logs, traces AND pipeline, and captureFromSearchHits captures
under whichever stream type the Logs page searched -- its selector covers
metrics and traces. Proved by searching cache_hit_ratio in the Logs UI, after
which the store held 21 metrics keys and the SLO form resolved environment ->
[development, staging].

Two names made that easy to get wrong, so both are fixed rather than just
apologised for:

  useFieldValueStore.ts -> fieldValueStore.ts

It is not a composable -- plain functions over IndexedDB, no reactive state --
and the false `use` prefix left it one character from useFieldValuesStream,
which IS a composable and does something else entirely. Dropping the prefix
also puts it next to its sibling fieldValueDB.ts, where it belongs.

The header now names BOTH writers, states that neither is tied to a stream type,
and spells out the trap directly: composables/useLogs/ is the Logs PAGE, not the
logs stream TYPE. StreamContext.streamType says the same at the field level --
part of the key, never a filter on what may be captured.

The rule worth remembering: values come from what has been SEARCHED or EXPANDED.
A stream nobody has looked at has none. That is a cold cache, not a missing
capability -- which is exactly the distinction I failed to make, off one grep.

Mechanical rename across 10 files including the vi.mock paths. 140 passing in
the touched specs; type-check, prettier clean; the 13 eslint warnings in
IndexList.vue are pre-existing (identical count with this change stashed).
This commit is contained in:
Prabhat Sharma 2026-08-02 17:05:27 -07:00
parent 4d231b9e48
commit 449e2d2dda
11 changed files with 33 additions and 15 deletions

View File

@ -634,7 +634,7 @@ describe("QueryEditorDialog - N1 context keywords reach the editor", () => {
// Stored field values for the N1 value-context probe above. Only useSuggestions
// consumes this module, so mocking it does not affect the rest of the suite.
vi.mock("@/composables/useFieldValueStore", () => ({
vi.mock("@/composables/fieldValueStore", () => ({
getFieldValuesForSuggestion: vi.fn().mockResolvedValue(["error", "warn"]),
}));

View File

@ -2128,6 +2128,6 @@ describe("QueryConfig.vue", () => {
// Stored field values for the N1 value-context probe above. Only useSuggestions
// consumes this module, so mocking it does not affect the rest of the suite.
vi.mock("@/composables/useFieldValueStore", () => ({
vi.mock("@/composables/fieldValueStore", () => ({
getFieldValuesForSuggestion: vi.fn().mockResolvedValue(["error", "warn"]),
}));

View File

@ -55,7 +55,7 @@ vi.mock("@/services/stream", () => ({
const { getFieldValuesForSuggestion } = vi.hoisted(() => ({
getFieldValuesForSuggestion: vi.fn(async () => ["ERROR", "INFO"]),
}));
vi.mock("@/composables/useFieldValueStore", () => ({ getFieldValuesForSuggestion }));
vi.mock("@/composables/fieldValueStore", () => ({ getFieldValuesForSuggestion }));
vi.mock("@/components/dashboards/PanelSchemaRenderer.vue", () => ({
default: { template: '<div data-test="panel-schema-renderer" />' },

View File

@ -23,7 +23,7 @@ import {
captureFromSearchHits,
captureFromValuesApi,
getFieldValuesForSuggestion,
} from "./useFieldValueStore";
} from "./fieldValueStore";
const ctx = { org: "myorg", streamType: "logs", streamName: "http_logs" };

View File

@ -14,9 +14,22 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/**
* Composable that orchestrates field value capture from both data sources:
* 1. Values API field expansion in FieldList
* 2. Search result hits Run Query results
* Persisted field VALUES, for autocomplete. Not a composable plain functions
* over IndexedDB, which is why it carries no `use` prefix and sits beside
* fieldValueDB.ts. (It was `useFieldValueStore`, one character from the real
* composable `useFieldValuesStream`, and the two were mistaken for each other.)
*
* THE ONLY TWO WRITERS, and neither is tied to a stream type:
* 1. captureFromValuesApi expanding a field in the sidebar. Reached from
* logs, traces and pipeline, via useFieldValuesStream.
* 2. captureFromSearchHits Run Query results. Its one caller lives under
* composables/useLogs/, which is the Logs PAGE, not the logs stream TYPE:
* it captures under whichever type was searched, and that page's stream
* selector covers metrics and traces too.
*
* So the rule is "values come from what has been SEARCHED or EXPANDED", never
* "values are logs-only". A stream nobody has looked at yet simply has none;
* that is a cold cache, not a missing capability.
*
* All IndexedDB writes are scheduled via requestIdleCallback so they have
* zero impact on main-thread rendering. Reads use an in-memory cache
@ -52,7 +65,12 @@ const READ_CACHE_MAX_ENTRIES = 500;
export interface StreamContext {
org: string;
streamType: string; // 'logs' | 'metrics' | 'traces'
/**
* Whichever type the caller searched or expanded all three are stored and
* read back the same way. Part of the key, never a filter on what may be
* captured.
*/
streamType: "logs" | "metrics" | "traces" | (string & {});
streamName: string;
}

View File

@ -17,7 +17,7 @@ import { ref, type Ref } from "vue";
import store from "@/stores";
import { generateTraceContext } from "@/utils/zincutils";
import useHttpStreaming from "@/composables/useStreamingSearch";
import { captureFromValuesApi } from "@/composables/useFieldValueStore";
import { captureFromValuesApi } from "@/composables/fieldValueStore";
export interface FieldValueEntry {
key: string;

View File

@ -23,7 +23,7 @@ import config from "@/aws-exports";
import { searchState } from "@/composables/useLogs/searchState";
import useStreams from "@/composables/useStreams";
import useSqlSuggestions from "@/composables/useSuggestions";
import { captureFromSearchHits } from "@/composables/useFieldValueStore";
import { captureFromSearchHits } from "@/composables/fieldValueStore";
import {
useLocalLogFilterField,

View File

@ -28,7 +28,7 @@ vi.mock("vuex", async (importOriginal) => {
return { ...actual, useStore: vi.fn(() => mockStore) };
});
vi.mock("@/composables/useFieldValueStore", () => ({
vi.mock("@/composables/fieldValueStore", () => ({
getFieldValuesForSuggestion: vi.fn().mockResolvedValue([]),
}));

View File

@ -12,7 +12,7 @@ vi.mock("vuex", async (importOriginal) => {
});
// ─── Mock IDB so no real storage is touched ───────────────────────────────────
vi.mock("@/composables/useFieldValueStore", () => ({
vi.mock("@/composables/fieldValueStore", () => ({
getFieldValuesForSuggestion: vi.fn().mockResolvedValue([]),
}));
@ -23,7 +23,7 @@ vi.mock("@/services/query_functions", () => ({
default: { list: vi.fn().mockResolvedValue({ data: { list: [] } }) },
}));
import { getFieldValuesForSuggestion } from "@/composables/useFieldValueStore";
import { getFieldValuesForSuggestion } from "@/composables/fieldValueStore";
import useSqlSuggestions from "./useSuggestions";
// ─── helper: build composable with common defaults ────────────────────────────

View File

@ -1,6 +1,6 @@
import { ref, computed } from "vue";
import { useStore } from "vuex";
import { getFieldValuesForSuggestion } from "@/composables/useFieldValueStore";
import { getFieldValuesForSuggestion } from "@/composables/fieldValueStore";
import {
SQL_KEYWORDS,
SQL_CLAUSE_KEYWORDS,

View File

@ -294,7 +294,7 @@ import OSelect from "@/lib/forms/Select/OSelect.vue";
import type { SelectModelValue } from "@/lib/forms/Select/OSelect.types";
import OSkeleton from "@/lib/feedback/Skeleton/OSkeleton.vue";
import OEmptyState from "@/lib/core/EmptyState/OEmptyState.vue";
import { captureFromValuesApi } from "@/composables/useFieldValueStore";
import { captureFromValuesApi } from "@/composables/fieldValueStore";
import { saveLogsStreamType, saveLogsStream } from "@/utils/streamPersist";
import { quoteSqlIdentifierIfNeeded } from "@/utils/query/sqlIdentifiers";
import { toast } from "@/lib/feedback/Toast/useToast";