From aa53b6457d1c2bc51a616c6795f3d8a92d134294 Mon Sep 17 00:00:00 2001 From: Seyed Mahmoud SHAHROKNI Date: Wed, 29 Apr 2026 13:17:41 +0200 Subject: [PATCH] [BREAKINGCHANGE] remove fetch from usage metrics Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI Signed-off-by: Seyed Mahmoud SHAHROKNI --- .../context/DatasourceStoreProvider.test.tsx | 30 ++++- dashboards/src/test/datasource-provider.tsx | 3 +- .../src/remote/remotePluginLoader.ts | 4 + .../src/runtime/UsageMetricsProvider.tsx | 105 ++++++++++++------ 4 files changed, 105 insertions(+), 37 deletions(-) diff --git a/dashboards/src/context/DatasourceStoreProvider.test.tsx b/dashboards/src/context/DatasourceStoreProvider.test.tsx index 9d8e65c3..86d35edc 100644 --- a/dashboards/src/context/DatasourceStoreProvider.test.tsx +++ b/dashboards/src/context/DatasourceStoreProvider.test.tsx @@ -24,9 +24,37 @@ import { DatasourceStoreProvider } from '@perses-dev/dashboards'; import { PropsWithChildren, ReactElement } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { DashboardSpec, DatasourceSpec, UnknownSpec } from '@perses-dev/spec'; -import { Datasource, DatasourceResource, GlobalDatasourceResource } from '@perses-dev/core'; import { DashboardResource } from '../model/DashboardResource'; +/* TODO: All GlobalDatasourceResource, DatasourceResource, etc ... + have been used for the test purpose only, creating a wrong dependency to the core! + We either move this test (somehow) to the Perses/UI or we need to duplicate the types +*/ +interface ProjectMetadata extends Metadata { + project: string; +} + +interface Metadata { + name: string; + createdAt?: string; + updatedAt?: string; + version?: number; + tags?: string[]; +} + +interface GlobalDatasourceResource { + kind: 'GlobalDatasource'; + metadata: Metadata; + spec: DatasourceSpec; +} + +interface DatasourceResource { + kind: 'Datasource'; + metadata: ProjectMetadata; + spec: DatasourceSpec; +} +type Datasource = DatasourceResource | GlobalDatasourceResource; + const PROJECT = 'perses'; const FAKE_PLUGIN_NAME = 'FakeDatasourcePlugin'; diff --git a/dashboards/src/test/datasource-provider.tsx b/dashboards/src/test/datasource-provider.tsx index d6a15008..06773f3f 100644 --- a/dashboards/src/test/datasource-provider.tsx +++ b/dashboards/src/test/datasource-provider.tsx @@ -11,12 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { GlobalDatasourceResource } from '@perses-dev/core'; import { DatasourceStoreProviderProps } from '../context'; import { getTestDashboard } from './dashboard-provider'; export const prometheusDemoUrl = 'https://prometheus.demo.prometheus.io'; -export const prometheusDemo: GlobalDatasourceResource = { +export const prometheusDemo = { kind: 'GlobalDatasource', metadata: { name: 'PrometheusDemo', diff --git a/plugin-system/src/remote/remotePluginLoader.ts b/plugin-system/src/remote/remotePluginLoader.ts index 88c61ba4..8b5d4c5c 100644 --- a/plugin-system/src/remote/remotePluginLoader.ts +++ b/plugin-system/src/remote/remotePluginLoader.ts @@ -85,6 +85,10 @@ export function remotePluginLoader(options?: RemotePluginLoaderOptions): PluginL return { getInstalledPlugins: async (): Promise => { + /* TODO: This is already using the globalThis fetch and not the core one + So, we don't have any dependency to the core here. + The question is if the developer did that intentionally?! + */ const pluginsResponse = await fetch(pluginsApiPath); const plugins = await pluginsResponse.json(); diff --git a/plugin-system/src/runtime/UsageMetricsProvider.tsx b/plugin-system/src/runtime/UsageMetricsProvider.tsx index 309dafdd..9ed2c958 100644 --- a/plugin-system/src/runtime/UsageMetricsProvider.tsx +++ b/plugin-system/src/runtime/UsageMetricsProvider.tsx @@ -11,42 +11,83 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { fetch } from '@perses-dev/core'; // TODO import { QueryDefinition } from '@perses-dev/spec'; -import { createContext, ReactElement, ReactNode, useContext } from 'react'; +import { createContext, ReactElement, ReactNode, useContext, useEffect, useState } from 'react'; type QueryState = 'pending' | 'success' | 'error'; -interface UsageMetrics { +export interface UsageMetrics { + project: string; + dashboard: string; + renderDurationMs: number; + renderErrorCount: number; +} + +interface UsageMetricsContext { project: string; dashboard: string; startRenderTime: number; renderDurationMs: number; + setRenderDurationMs: React.Dispatch>; renderErrorCount: number; + setRenderErrorCount: React.Dispatch>; pendingQueries: Map; + setPendingQueries: React.Dispatch>>; apiPrefix?: string; + submitMetrics: (metrics: UsageMetrics) => Promise; } -interface UsageMetricsProps { +export interface UsageMetricsProps { project: string; dashboard: string; apiPrefix?: string; children: ReactNode; + submitMetrics: (metrics: UsageMetrics) => Promise; } interface UseUsageMetricsResults { markQuery: (definition: QueryDefinition, state: QueryState) => void; } -export const UsageMetricsContext = createContext(undefined); +export const UsageMetricsContext = createContext(undefined); -export const useUsageMetricsContext = (): UsageMetrics | undefined => { +export const useUsageMetricsContext = (): UsageMetricsContext | undefined => { return useContext(UsageMetricsContext); }; export const useUsageMetrics = (): UseUsageMetricsResults => { const ctx = useUsageMetricsContext(); + useEffect(() => { + if (!ctx) { + return; + } + const { + dashboard, + project, + renderErrorCount, + pendingQueries, + renderDurationMs, + startRenderTime, + submitMetrics, + setRenderDurationMs, + } = ctx; + + /* This means no query has run yet, so it should return + The subsequent logic makes sense when a-some queries have been running + */ + if (!pendingQueries.size) { + return; + } + + const allDone = [...pendingQueries.values()].every((p) => p !== 'pending'); + if (renderDurationMs === 0 && allDone) { + const finalDuration = Date.now() - startRenderTime; + setRenderDurationMs(finalDuration); + submitMetrics({ dashboard, project, renderDurationMs, renderErrorCount }); + } + }, [ctx]); + return { markQuery: (definition: QueryDefinition, newState: QueryState): void => { if (ctx === undefined) { @@ -60,45 +101,41 @@ export const useUsageMetrics = (): UseUsageMetricsResults => { } if (ctx.pendingQueries.get(definitionKey) !== newState) { - ctx.pendingQueries.set(definitionKey, newState); + ctx.setPendingQueries((prev) => { + const map = new Map(prev); + map.set(definitionKey, newState); + return map; + }); if (newState === 'error') { - ctx.renderErrorCount += 1; - } - - const allDone = [...ctx.pendingQueries.values()].every((p) => p !== 'pending'); - if (ctx.renderDurationMs === 0 && allDone) { - ctx.renderDurationMs = Date.now() - ctx.startRenderTime; - submitMetrics(ctx); + ctx.setRenderErrorCount((prev) => prev + 1); } } }, }; }; -const submitMetrics = async (stats: UsageMetrics): Promise => { - await fetch(`${stats.apiPrefix ?? ''}/api/v1/view`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - project: stats.project, - dashboard: stats.dashboard, - render_time: stats.renderDurationMs / 1000, - render_errors: stats.renderErrorCount, - }), - }); -}; - -export const UsageMetricsProvider = ({ apiPrefix, project, dashboard, children }: UsageMetricsProps): ReactElement => { - const ctx: UsageMetrics = { +export const UsageMetricsProvider = ({ + apiPrefix, + project, + dashboard, + children, + submitMetrics, +}: UsageMetricsProps): ReactElement => { + const [renderDurationMs, setRenderDurationMs] = useState(0); + const [pendingQueries, setPendingQueries] = useState(new Map()); + const [renderErrorCount, setRenderErrorCount] = useState(0); + const ctx: UsageMetricsContext = { project: project, dashboard: dashboard, - renderErrorCount: 0, + renderErrorCount, startRenderTime: Date.now(), - renderDurationMs: 0, - pendingQueries: new Map(), + renderDurationMs, + setRenderDurationMs, + setPendingQueries, + setRenderErrorCount, + pendingQueries, apiPrefix, + submitMetrics, }; return {children};