Skip to content

Commit b7d287e

Browse files
committed
feat(webapp): keep the queue-metrics tables unlisted until they are rolled out
The Query page, its schema docs, the schema API and the AI query context listed queue_metrics and env_metrics as soon as this deployed, so the tables were advertised to everyone while ingestion was still off and they were empty. Listing them is now behind QUEUE_METRICS_QUERY_TABLES_VISIBLE, off by default. Listing only, matching how the existing hidden table already behaves: a query that names one of these tables still compiles and runs, with tenancy enforced as usual, so the dashboards and the health report keep working while the tables are unlisted. The filter is a pure function rather than a precomputed list, because three of the five callers are browser components and that module cannot reach for env.server. Server callers read the env var; client callers read the flag through the existing features hook.
1 parent 11e6fb8 commit b7d287e

9 files changed

Lines changed: 83 additions & 20 deletions

File tree

apps/webapp/app/components/query/QueryEditor.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
useCallback,
1111
useEffect,
1212
useImperativeHandle,
13+
useMemo,
1314
useRef,
1415
useState,
1516
type ReactNode,
@@ -72,7 +73,8 @@ import type { action as titleAction } from "~/routes/resources.orgs.$organizatio
7273
import type { QueryScope } from "~/services/queryService.server";
7374
import { downloadFile, rowsToCSV, rowsToJSON } from "~/utils/dataExport";
7475
import { organizationBillingPath } from "~/utils/pathBuilder";
75-
import { visibleQuerySchemas } from "~/v3/querySchemas";
76+
import { listableQuerySchemas } from "~/v3/querySchemas";
77+
import { useFeatures } from "~/hooks/useFeatures";
7678

7779
/** Convert a Date or ISO string to ISO string format */
7880
function toISOString(value: Date | string): string {
@@ -187,6 +189,11 @@ const QueryEditorForm = forwardRef<
187189
ref
188190
) {
189191
const isLoading = fetcher.state === "submitting" || fetcher.state === "loading";
192+
const { queueMetricsQueryTables } = useFeatures();
193+
const schemas = useMemo(
194+
() => listableQuerySchemas({ includeQueueMetrics: queueMetricsQueryTables }),
195+
[queueMetricsQueryTables]
196+
);
190197
const [query, setQuery] = useState(defaultQuery);
191198
const [scope, setScope] = useState<QueryScope>(defaultScope);
192199
const formRef = useRef<HTMLFormElement>(null);
@@ -245,7 +252,7 @@ const QueryEditorForm = forwardRef<
245252
<TSQLEditor
246253
defaultValue={query}
247254
onChange={setQuery}
248-
schema={visibleQuerySchemas}
255+
schema={schemas}
249256
linterEnabled={true}
250257
showCopyButton={true}
251258
showClearButton={true}

apps/webapp/app/env.server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,12 @@ const EnvironmentSchema = z
942942
// Fraction (0..1) of ops that emit a gauge; counters are never sampled. Dial below 1
943943
// only if EngineCPU is too high in slow-path-heavy regions (hurts low-traffic queues).
944944
QUEUE_METRICS_GAUGE_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(1),
945+
/**
946+
* Lists the queue-metrics tables in the Query page, its schema docs, the schema API and the
947+
* AI query context. Off by default so the tables are not advertised before ingestion is
948+
* enabled. Listing only: a query naming one of these tables still runs either way.
949+
*/
950+
QUEUE_METRICS_QUERY_TABLES_VISIBLE: z.string().default("0"),
945951
RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000),
946952
RUN_ENGINE_RETRY_WARM_START_THRESHOLD_MS: z.coerce.number().int().default(30_000),
947953
RUN_ENGINE_PROCESS_WORKER_QUEUE_DEBOUNCE_MS: z.coerce.number().int().default(200),

apps/webapp/app/features.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { requestUrl } from "./utils/requestUrl.server";
44
export type TriggerFeatures = {
55
isManagedCloud: boolean;
66
hasPrivateConnections: boolean;
7+
queueMetricsQueryTables: boolean;
78
};
89

910
function isManagedCloud(host: string): boolean {
@@ -26,6 +27,7 @@ function featuresForHost(host: string): TriggerFeatures {
2627
return {
2728
isManagedCloud: isManagedCloud(host),
2829
hasPrivateConnections: hasPrivateConnections(host),
30+
queueMetricsQueryTables: env.QUEUE_METRICS_QUERY_TABLES_VISIBLE === "1",
2931
};
3032
}
3133

apps/webapp/app/hooks/useFeatures.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@ import type { TriggerFeatures } from "~/features.server";
55
export function useFeatures(): TriggerFeatures {
66
const routeMatch = useTypedRouteLoaderData<typeof loader>("root");
77

8-
return routeMatch?.features ?? { isManagedCloud: false, hasPrivateConnections: false };
8+
return (
9+
routeMatch?.features ?? {
10+
isManagedCloud: false,
11+
hasPrivateConnections: false,
12+
queueMetricsQueryTables: false,
13+
}
14+
);
915
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useState } from "react";
1+
import { useMemo, useState } from "react";
22
import { Header3 } from "~/components/primitives/Headers";
33
import { Paragraph } from "~/components/primitives/Paragraph";
44
import SegmentedControl from "~/components/primitives/SegmentedControl";
55
import type { QueryScope } from "~/services/queryService.server";
6-
import { visibleQuerySchemas } from "~/v3/querySchemas";
6+
import { listableQuerySchemas } from "~/v3/querySchemas";
7+
import { useFeatures } from "~/hooks/useFeatures";
78
import { TryableCodeBlock } from "./TRQLGuideContent";
89

910
// Example queries for the Examples tab
@@ -211,14 +212,21 @@ LIMIT 20`,
211212
},
212213
];
213214

214-
const tableOptions = visibleQuerySchemas.map((s) => ({ label: s.name, value: s.name }));
215-
216215
export function ExamplesContent({
217216
onTryExample,
218217
}: {
219218
onTryExample: (query: string, scope: QueryScope) => void;
220219
}) {
221-
const [selectedTable, setSelectedTable] = useState(visibleQuerySchemas[0].name);
220+
const { queueMetricsQueryTables } = useFeatures();
221+
const schemas = useMemo(
222+
() => listableQuerySchemas({ includeQueueMetrics: queueMetricsQueryTables }),
223+
[queueMetricsQueryTables]
224+
);
225+
const tableOptions = useMemo(
226+
() => schemas.map((s) => ({ label: s.name, value: s.name })),
227+
[schemas]
228+
);
229+
const [selectedTable, setSelectedTable] = useState(schemas[0].name);
222230
const filtered = exampleQueries.filter((e) => e.table === selectedTable);
223231

224232
return (

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/TableSchemaContent.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { ColumnSchema } from "@internal/tsql";
2-
import { useState } from "react";
2+
import { useMemo, useState } from "react";
33
import { Badge } from "~/components/primitives/Badge";
44
import { CopyableText } from "~/components/primitives/CopyableText";
55
import { Paragraph } from "~/components/primitives/Paragraph";
66
import SegmentedControl from "~/components/primitives/SegmentedControl";
7-
import { visibleQuerySchemas } from "~/v3/querySchemas";
7+
import { listableQuerySchemas } from "~/v3/querySchemas";
8+
import { useFeatures } from "~/hooks/useFeatures";
89

910
function ColumnHelpItem({ col }: { col: ColumnSchema }) {
1011
return (
@@ -43,11 +44,18 @@ function ColumnHelpItem({ col }: { col: ColumnSchema }) {
4344
);
4445
}
4546

46-
const tableOptions = visibleQuerySchemas.map((s) => ({ label: s.name, value: s.name }));
47-
4847
export function TableSchemaContent() {
49-
const [selectedTable, setSelectedTable] = useState(visibleQuerySchemas[0].name);
50-
const table = visibleQuerySchemas.find((s) => s.name === selectedTable) ?? visibleQuerySchemas[0];
48+
const { queueMetricsQueryTables } = useFeatures();
49+
const schemas = useMemo(
50+
() => listableQuerySchemas({ includeQueueMetrics: queueMetricsQueryTables }),
51+
[queueMetricsQueryTables]
52+
);
53+
const tableOptions = useMemo(
54+
() => schemas.map((s) => ({ label: s.name, value: s.name })),
55+
[schemas]
56+
);
57+
const [selectedTable, setSelectedTable] = useState(schemas[0].name);
58+
const table = schemas.find((s) => s.name === selectedTable) ?? schemas[0];
5159

5260
return (
5361
<div>

apps/webapp/app/routes/api.v1.query.schema.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { json } from "@remix-run/server-runtime";
22
import type { ColumnSchema, TableSchema } from "@internal/tsql";
33
import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
4-
import { visibleQuerySchemas } from "~/v3/querySchemas";
4+
import { listableQuerySchemas } from "~/v3/querySchemas";
5+
import { env } from "~/env.server";
56

67
function serializeColumn(col: ColumnSchema) {
78
const result: Record<string, unknown> = {
@@ -51,7 +52,9 @@ export const loader = createLoaderApiRoute(
5152
},
5253
},
5354
async () => {
54-
const tables = visibleQuerySchemas.map(serializeTable);
55+
const tables = listableQuerySchemas({
56+
includeQueueMetrics: env.QUEUE_METRICS_QUERY_TABLES_VISIBLE === "1",
57+
}).map(serializeTable);
5558
return json({ tables });
5659
}
5760
);

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { AITimeFilter } from "~/routes/_app.orgs.$organizationSlug.projects
88
import { requireUserId } from "~/services/session.server";
99
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
1010
import { AIQueryService } from "~/v3/services/aiQueryService.server";
11-
import { visibleQuerySchemas } from "~/v3/querySchemas";
11+
import { listableQuerySchemas } from "~/v3/querySchemas";
1212

1313
const RequestSchema = z.object({
1414
prompt: z.string().min(1, "Prompt is required"),
@@ -85,7 +85,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
8585
const { prompt, mode, currentQuery } = submission.data;
8686

8787
const service = new AIQueryService(
88-
visibleQuerySchemas,
88+
listableQuerySchemas({
89+
includeQueueMetrics: env.QUEUE_METRICS_QUERY_TABLES_VISIBLE === "1",
90+
}),
8991
openai(env.AI_RUN_FILTER_MODEL ?? "gpt-4o-mini")
9092
);
9193

apps/webapp/app/v3/querySchemas.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,8 +1444,29 @@ export const querySchemas: TableSchema[] = [
14441444
queueMetricsByKeySchema,
14451445
];
14461446

1447-
/** Schemas shown in user-facing listings (editor autocomplete, schema docs, schema API). */
1448-
export const visibleQuerySchemas: TableSchema[] = querySchemas.filter((s) => !s.hidden);
1447+
/** Tables whose listing is deferred until queue metrics are rolled out. */
1448+
const QUEUE_METRICS_TABLE_NAMES = new Set([
1449+
queueMetricsSchema.name,
1450+
envMetricsSchema.name,
1451+
queueMetricsByKeySchema.name,
1452+
]);
1453+
1454+
/**
1455+
* Schemas shown in user-facing listings (editor autocomplete, schema docs, schema API, AI query
1456+
* context). Listing only: `querySchemas` stays the compile-time set, so a query naming an unlisted
1457+
* table still runs, with tenancy enforced as usual.
1458+
*
1459+
* Server callers pass `env.QUEUE_METRICS_QUERY_TABLES_VISIBLE === "1"`; client callers read
1460+
* `queueMetricsQueryTables` from `useFeatures()`. This module is imported by browser code, so it
1461+
* must not reach for `env.server` itself.
1462+
*/
1463+
export function listableQuerySchemas(options: { includeQueueMetrics: boolean }): TableSchema[] {
1464+
return querySchemas.filter((s) => {
1465+
if (s.hidden) return false;
1466+
if (!options.includeQueueMetrics && QUEUE_METRICS_TABLE_NAMES.has(s.name)) return false;
1467+
return true;
1468+
});
1469+
}
14491470

14501471
/**
14511472
* Default query for the query editor

0 commit comments

Comments
 (0)