From 9bef801150f6c25c184fd2e9774fc2d0958a3ae1 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Tue, 3 Sep 2024 16:27:16 +0200 Subject: [PATCH 01/12] feat: web vitals --- packages/main/helpers/webVitals.ts | 119 ++++++++++++++++++ packages/main/package.json | 2 + packages/main/src/App.tsx | 3 +- .../DataViews/components/Charts/QrynChart.tsx | 6 +- packages/main/src/main.tsx | 9 +- packages/main/vite.config.ts | 5 + packages/main/workers/useVitalsWorker.tsx | 17 +++ 7 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 packages/main/helpers/webVitals.ts create mode 100644 packages/main/workers/useVitalsWorker.tsx diff --git a/packages/main/helpers/webVitals.ts b/packages/main/helpers/webVitals.ts new file mode 100644 index 00000000..04f7741b --- /dev/null +++ b/packages/main/helpers/webVitals.ts @@ -0,0 +1,119 @@ +import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals"; + +const location = window.location; +const url = location.protocol + "//" + location.host; +export const LOKI_WRITE = url + "/loki/api/v1/push"; +export const METRICS_WRITE = url + "/influx/api/v2/write"; + +const formatWebVitalsMetrics = (queue_array) => { + return queue_array + .map( + (item) => + `${item.metric},level=info,rating=${item.rating} value=${item.value} ${item.timestamp}` + ) + .join("\n"); +}; + +const metrics_push = async (metrics_data: any) => { + console.log(metrics_data) + return await fetch(METRICS_WRITE, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: metrics_data, + }) + .then((data) => { + console.log(JSON.stringify(data)); + }) + .catch((e) => { + console.log(e); + }) + .finally(() => { + console.log("sent metrics"); + }); +}; + +const logs_push = async (logs_data: any) => { + await fetch(LOKI_WRITE, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + + body: logs_data, + }) + .then((response) => { + return response.json(); + }) + .catch((e) => console.log(e)) + .finally(() => { + console.log("web vitals sent"); + }); +}; + +const format_logs_queue = (queue: any) => { + + const mapped = Array.from(queue).map((entry: any) => ({ + stream: { + level: "info", + job: "webVitals", + name: entry.name, + value: entry.value, + rating: entry.rating, + delta: entry.delta, + }, + values: [ + [ + String(Date.now() * 1000000), + `job=WebVitals name=${entry.name} value=${entry.value} rating=${entry.rating} delta=${entry.delta} entries=${JSON.stringify(entry.entries)}`, + ], + ], + })); + if(Array.isArray(mapped) && mapped?.length > 0) { + return JSON.stringify({ streams: [...mapped] }); + } + return JSON.stringify({ streams: [] }); +}; + +const format_metrics_queue = (queue) => { + return Array.from(queue).map((entry: any) => ({ + metric: entry.name, + value: entry.value, + delta: entry.delta, + rating: entry.rating, + timestamp: Date.now() * 1_000_000, + })); +}; + +export async function flushQueue(queue) { + if (queue.size > 0) { + const logs_body = format_logs_queue(queue); + const queuearray = format_metrics_queue(queue); + await metrics_push(formatWebVitalsMetrics(queuearray)); + await logs_push(logs_body); + queue.clear(); + } +} + +export const handleVisibilityChange = async (queue) => { + if (document.visibilityState === "hidden") { + await flushQueue(queue); + } +}; + +export const reportWebVitals = (queue) => { + function addToQueue(metric) { + queue.add(metric); + } + onCLS(addToQueue); + onINP(addToQueue); + onFCP(addToQueue); + onTTFB(addToQueue); + onLCP(addToQueue); + + addEventListener( + "visibilitychange", + async () => await handleVisibilityChange(queue) + ); +}; diff --git a/packages/main/package.json b/packages/main/package.json index 483a8065..ff1c0c19 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -63,6 +63,7 @@ "slate-history": "^0.100.0", "slate-react": "^0.105.0", "tss-react": "^4.9.10", + "web-vitals": "^4.2.3", "zod": "^3.23.8", "zustand": "^4.5.2" }, @@ -76,6 +77,7 @@ "@typescript-eslint/parser": "^7.17.0", "@vitejs/plugin-basic-ssl": "^1.1.0", "@vitejs/plugin-react": "^4.3.1", + "dayjs-plugin-utc": "^0.1.2", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", diff --git a/packages/main/src/App.tsx b/packages/main/src/App.tsx index eba21ed8..e6f51e2b 100644 --- a/packages/main/src/App.tsx +++ b/packages/main/src/App.tsx @@ -6,7 +6,7 @@ import { Notification } from "@ui/qrynui/notifications"; import { useSelector } from "react-redux"; import SettingsDialog from "@ui/plugins/settingsdialog/SettingsDialog"; import { QrynTheme } from "@ui/theme/types"; - +import { useWebVitals } from '../workers/useVitalsWorker' export const MainAppStyles = (theme:QrynTheme) => css` background: ${theme.background}; display:flex; @@ -18,6 +18,7 @@ export const MainAppStyles = (theme:QrynTheme) => css` export default function App() { const theme = useTheme(); const settingDialogOpen = useSelector((store:any)=>store.settingsDialogOpen) + useWebVitals() return (
diff --git a/packages/main/src/components/DataViews/components/Charts/QrynChart.tsx b/packages/main/src/components/DataViews/components/Charts/QrynChart.tsx index 2c31e624..f210b46b 100644 --- a/packages/main/src/components/DataViews/components/Charts/QrynChart.tsx +++ b/packages/main/src/components/DataViews/components/Charts/QrynChart.tsx @@ -442,7 +442,7 @@ export default function QrynChart(props: QrynChartProps) { barWidth, isLogsVolume && type === "stream" ); - + if(min && max && timeformat) { let plot = $q.plot( element, newData, @@ -451,10 +451,12 @@ export default function QrynChart(props: QrynChartProps) { xaxis: { timeformat, min, max }, }) ); - const colorLabels = plot.getData(); setLabels(colorLabels); $q(chartRef.current).UseTooltip(plot); + } + + } catch (e) { console.log(e); } diff --git a/packages/main/src/main.tsx b/packages/main/src/main.tsx index cfbbbe6a..9a9ce70b 100644 --- a/packages/main/src/main.tsx +++ b/packages/main/src/main.tsx @@ -6,11 +6,16 @@ import "./scss/app.scss"; import errorInterceptor from "@ui/helpers/error.interceptor"; import { Notification } from "@ui/qrynui/notifications"; - import { CookiesProvider } from "react-cookie"; -import { Routes, Route, HashRouter } from "react-router-dom"; +import { + Routes, + Route, + HashRouter, +} from "react-router-dom"; + import { lazy, Suspense } from "react"; + import ScreenLoader from "@ui/views/ScreenLoader"; import store from "@ui/store/store"; diff --git a/packages/main/vite.config.ts b/packages/main/vite.config.ts index cbe20de1..5e5a91ee 100644 --- a/packages/main/vite.config.ts +++ b/packages/main/vite.config.ts @@ -120,6 +120,11 @@ export default defineConfig(({ mode }) => { changeOrigin: env.VITE_API_BASE_URL, secure: false, }, + "/influx": { + target: proxyApi, + changeOrigin: env.VITE_API_BASE_URL, + secure: false, + }, "/ready": { target: proxyApi, changeOrigin: env.VITE_API_BASE_URL, diff --git a/packages/main/workers/useVitalsWorker.tsx b/packages/main/workers/useVitalsWorker.tsx new file mode 100644 index 00000000..923a87bd --- /dev/null +++ b/packages/main/workers/useVitalsWorker.tsx @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import {reportWebVitals, flushQueue,handleVisibilityChange } from '../helpers/webVitals' + +export const useWebVitals = () => { + useEffect(() => { + // Queue Set + const metricsQueue = new Set(); + reportWebVitals(metricsQueue) + return () => { + document.removeEventListener( + "visibilitychange", + handleVisibilityChange + ); + flushQueue(metricsQueue); + }; + }, []); +}; From 92ca07a6609cd664da821bbd8de8d1d610ad793f Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Fri, 6 Sep 2024 18:29:34 +0200 Subject: [PATCH 02/12] fix: web vitals hook --- packages/main/plugins/Plugins.tsx | 2 + packages/main/src/App.tsx | 4 +- packages/main/tsconfig.json | 3 +- packages/main/util/useWebVitals.tsx | 21 +++ packages/main/util/webVitals.ts | 122 ++++++++++++++++++ .../main/views/DataSources/DataSources.tsx | 5 +- packages/main/views/Main.tsx | 4 + pnpm-lock.yaml | 16 +++ 8 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 packages/main/util/useWebVitals.tsx create mode 100644 packages/main/util/webVitals.ts diff --git a/packages/main/plugins/Plugins.tsx b/packages/main/plugins/Plugins.tsx index d6529807..a7c91f03 100644 --- a/packages/main/plugins/Plugins.tsx +++ b/packages/main/plugins/Plugins.tsx @@ -4,9 +4,11 @@ import useTheme from "@ui/theme/useTheme"; import { LocalPluginsManagement } from "./PluginManagerFactory"; import { PluginPageStyles } from "./PluginStyles"; import { PluginCards } from "./PluginCards"; +import { useWebVitals } from "@util/useWebVitals"; export default function Plugins() { const theme = useTheme(); + useWebVitals({page:"Plugins"}); const pl = LocalPluginsManagement(); const [local] = useState(pl.getAll()); const plugins: any = useMemo(() => { diff --git a/packages/main/src/App.tsx b/packages/main/src/App.tsx index e6f51e2b..5107fbc9 100644 --- a/packages/main/src/App.tsx +++ b/packages/main/src/App.tsx @@ -6,7 +6,7 @@ import { Notification } from "@ui/qrynui/notifications"; import { useSelector } from "react-redux"; import SettingsDialog from "@ui/plugins/settingsdialog/SettingsDialog"; import { QrynTheme } from "@ui/theme/types"; -import { useWebVitals } from '../workers/useVitalsWorker' +import { useWebVitals } from '../util/useWebVitals' export const MainAppStyles = (theme:QrynTheme) => css` background: ${theme.background}; display:flex; @@ -18,7 +18,7 @@ export const MainAppStyles = (theme:QrynTheme) => css` export default function App() { const theme = useTheme(); const settingDialogOpen = useSelector((store:any)=>store.settingsDialogOpen) - useWebVitals() + useWebVitals({page:"App"}) return (
diff --git a/packages/main/tsconfig.json b/packages/main/tsconfig.json index 02908883..00c7583e 100644 --- a/packages/main/tsconfig.json +++ b/packages/main/tsconfig.json @@ -33,9 +33,10 @@ "@ui/theme/*": ["theme/*"], "@ui/qrynui/*": ["qrynui/*"], "@ui/environment/*":["environment/*"], + "@util/*":["util/*"], "react": ["./node_modules/@types/react"], } }, - "include": ["src","views"], + "include": ["src","views","util"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/packages/main/util/useWebVitals.tsx b/packages/main/util/useWebVitals.tsx new file mode 100644 index 00000000..c6584cc1 --- /dev/null +++ b/packages/main/util/useWebVitals.tsx @@ -0,0 +1,21 @@ +import { useEffect } from "react"; +import {reportWebVitals, flushQueue,handleVisibilityChange } from './webVitals' + +export type WebVitalsProps = { + page?: string; +} + +export const useWebVitals = ({page}:WebVitalsProps) => { + useEffect(() => { + // Queue Set + const metricsQueue = new Set(); + reportWebVitals(metricsQueue, page) + return () => { + document.removeEventListener( + "visibilitychange", + handleVisibilityChange + ); + flushQueue(metricsQueue); + }; + }, []); +}; diff --git a/packages/main/util/webVitals.ts b/packages/main/util/webVitals.ts new file mode 100644 index 00000000..aba136dc --- /dev/null +++ b/packages/main/util/webVitals.ts @@ -0,0 +1,122 @@ +import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals"; + +const location = window.location; +const url = location.protocol + "//" + location.host; +export const LOKI_WRITE = url + "/loki/api/v1/push"; +export const METRICS_WRITE = url + "/influx/api/v2/write"; + +const formatWebVitalsMetrics = (queue_array) => { + return queue_array + .map( + (item) => + `${item.metric},level=info,page=${item.page},rating=${item.rating} value=${item.value} ${item.timestamp}` + ) + .join("\n"); +}; + +const metrics_push = async (metrics_data: any) => { + console.log(metrics_data); + return await fetch(METRICS_WRITE, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: metrics_data, + }) + .then((data) => { + console.log(JSON.stringify(data)); + }) + .catch((e) => { + console.log(e); + }) + .finally(() => { + console.log("sent metrics"); + }); +}; + +const logs_push = async (logs_data: any) => { + await fetch(LOKI_WRITE, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + + body: logs_data, + }) + .then((response) => { + return response.json(); + }) + .catch((e) => console.log(e)) + .finally(() => { + console.log("web vitals sent"); + }); +}; +// add page name to entry +const format_logs_queue = (queue: any) => { + const mapped = Array.from(queue).map(({ metric, page }: any) => ({ + stream: { + level: "info", + job: "webVitals", + name: metric.name, + value: metric.value, + rating: metric.rating, + delta: metric.delta, + page: page ?? "", + }, + values: [ + [ + String(Date.now() * 1000000), + `job=WebVitals name=${metric.name} page=${page ?? ""} value=${metric.value} rating=${metric.rating} delta=${metric.delta} entries=${JSON.stringify(metric.entries)}`, + ], + ], + })); + if (Array.isArray(mapped) && mapped?.length > 0) { + return JSON.stringify({ streams: [...mapped] }); + } + return JSON.stringify({ streams: [] }); +}; +// add page name to entry +const format_metrics_queue = (queue) => { + return Array.from(queue).map(({ metric, page }: any) => ({ + metric: metric.name, + page: page ?? "", + value: metric.value, + delta: metric.delta, + rating: metric.rating, + timestamp: Date.now() * 1_000_000, + })); +}; + +export async function flushQueue(queue) { + if (queue.size > 0) { + const logs_body = format_logs_queue(queue); + const queuearray = format_metrics_queue(queue); + await metrics_push(formatWebVitalsMetrics(queuearray)); + await logs_push(logs_body); + queue.clear(); + } +} + +export const handleVisibilityChange = async (queue) => { + console.log(queue); + if (document.visibilityState === "hidden") { + await flushQueue(queue); + } +}; + +export const reportWebVitals = (queue, page) => { + console.log(queue, page); + function addToQueue(metric) { + queue.add(metric); + } + onCLS((metric) => addToQueue({ metric, page })); + onINP((metric) => addToQueue({ metric, page })); + onFCP((metric) => addToQueue({ metric, page })); + onTTFB((metric) => addToQueue({ metric, page })); + onLCP((metric) => addToQueue({ metric, page })); + + addEventListener( + "visibilitychange", + async () => await handleVisibilityChange(queue) + ); +}; diff --git a/packages/main/views/DataSources/DataSources.tsx b/packages/main/views/DataSources/DataSources.tsx index a898fb7b..9b1b2655 100644 --- a/packages/main/views/DataSources/DataSources.tsx +++ b/packages/main/views/DataSources/DataSources.tsx @@ -9,7 +9,7 @@ import { DataSourcesFiller } from "./components/DataSourcesFiller"; import { setTheme } from "@ui/store/actions"; import { useMediaQuery } from "react-responsive"; import useTheme from "@ui/theme/useTheme" - +import { useWebVitals} from '@util/useWebVitals' export function getURlParams(params: any) { const url = params.replace(/#/, ""); const decoded = decodeURIComponent(url); @@ -27,6 +27,9 @@ export default function DataSources() { const dispatch: any = useDispatch(); const theme = useTheme(); const autoTheme = useSelector((store: any) => store.autoTheme); + + useWebVitals({page:"DataSources"}) + useEffect(() => { if (autoTheme) { const theme = isAutoDark ? "dark" : "light"; diff --git a/packages/main/views/Main.tsx b/packages/main/views/Main.tsx index c08a2960..bb48801f 100644 --- a/packages/main/views/Main.tsx +++ b/packages/main/views/Main.tsx @@ -16,6 +16,7 @@ import { import { css, cx } from "@emotion/css"; import useTheme from "@ui/theme/useTheme"; import { setCurrentUser } from "./User/actions"; +import { useWebVitals } from '@util/useWebVitals' const MainStyles = (theme: any) => css` background: ${theme.shadow}; @@ -24,6 +25,9 @@ const MainStyles = (theme: any) => css` export default function Main() { const navigate = useNavigate(); const dataSources = useSelector((store: any) => store.dataSources); + + useWebVitals({page:"Main"}); + // get hash from current location const { hash } = useLocation(); // get url params as object diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5424298f..2fe8ad44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: tss-react: specifier: ^4.9.10 version: 4.9.10(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.20(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + web-vitals: + specifier: ^4.2.3 + version: 4.2.3 zod: specifier: ^3.23.8 version: 3.23.8 @@ -226,6 +229,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.1 version: 4.3.1(vite@5.3.1(@types/node@20.14.8)(sass@1.77.6)) + dayjs-plugin-utc: + specifier: ^0.1.2 + version: 0.1.2 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -1440,6 +1446,9 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dayjs-plugin-utc@0.1.2: + resolution: {integrity: sha512-ExERH5o3oo6jFOdkvMP3gytTCQ9Ksi5PtylclJWghr7k7m3o2U5QrwtdiJkOxLOH4ghr0EKhpqGefzGz1VvVJg==} + dayjs@1.11.12: resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==} @@ -2939,6 +2948,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + web-vitals@4.2.3: + resolution: {integrity: sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4324,6 +4336,8 @@ snapshots: date-fns@3.6.0: {} + dayjs-plugin-utc@0.1.2: {} + dayjs@1.11.12: {} debug@4.3.5: @@ -5916,6 +5930,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + web-vitals@4.2.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} From 1329d21891857231c38e7c0e40a46dbb1e0ee466 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Wed, 11 Sep 2024 18:19:27 +0200 Subject: [PATCH 03/12] fix: web vitals as a plugin --- README.md | 6 + packages/main/helpers/webVitals.ts | 119 ------ packages/main/package.json | 1 + packages/main/plugins/PluginCards.tsx | 2 +- packages/main/plugins/PluginStyles.tsx | 27 +- packages/main/plugins/Plugins.tsx | 29 +- packages/main/plugins/PluginsRenderer.tsx | 13 +- packages/main/plugins/WebVitals/WebVitals.tsx | 35 ++ packages/main/plugins/WebVitals/index.tsx | 15 + packages/main/plugins/WebVitals/store.ts | 18 + .../main/plugins/WebVitals/useWebVitals.tsx | 33 ++ packages/main/plugins/WebVitals/webVitals.ts | 403 ++++++++++++++++++ packages/main/plugins/index.tsx | 15 +- packages/main/src/App.tsx | 26 +- packages/main/tsconfig.json | 2 +- packages/main/util/useWebVitals.tsx | 21 - packages/main/util/webVitals.ts | 122 ------ .../main/views/DataSources/DataSources.tsx | 21 +- .../main/views/DataSources/getUrlParams.ts | 11 + packages/main/views/Main.tsx | 30 +- packages/main/views/Main/MainStatusBar.tsx | 6 +- packages/main/vite.config.ts | 6 + packages/main/workers/useVitalsWorker.tsx | 17 - pnpm-lock.yaml | 9 + 24 files changed, 635 insertions(+), 352 deletions(-) delete mode 100644 packages/main/helpers/webVitals.ts create mode 100644 packages/main/plugins/WebVitals/WebVitals.tsx create mode 100644 packages/main/plugins/WebVitals/index.tsx create mode 100644 packages/main/plugins/WebVitals/store.ts create mode 100644 packages/main/plugins/WebVitals/useWebVitals.tsx create mode 100644 packages/main/plugins/WebVitals/webVitals.ts delete mode 100644 packages/main/util/useWebVitals.tsx delete mode 100644 packages/main/util/webVitals.ts create mode 100644 packages/main/views/DataSources/getUrlParams.ts delete mode 100644 packages/main/workers/useVitalsWorker.tsx diff --git a/README.md b/README.md index 681837af..da9cf2ac 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,12 @@ _JSON stringifyed and URL encoded_ ------------ +### Local Proxy for headeless qryn-view (for experimentation purposes only): + +Add to you .env file: + +`VITE_API_BASE_URL= { your local qryn api }` +should we with same protocol as your qryn-view instance ### About qryn diff --git a/packages/main/helpers/webVitals.ts b/packages/main/helpers/webVitals.ts deleted file mode 100644 index 04f7741b..00000000 --- a/packages/main/helpers/webVitals.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals"; - -const location = window.location; -const url = location.protocol + "//" + location.host; -export const LOKI_WRITE = url + "/loki/api/v1/push"; -export const METRICS_WRITE = url + "/influx/api/v2/write"; - -const formatWebVitalsMetrics = (queue_array) => { - return queue_array - .map( - (item) => - `${item.metric},level=info,rating=${item.rating} value=${item.value} ${item.timestamp}` - ) - .join("\n"); -}; - -const metrics_push = async (metrics_data: any) => { - console.log(metrics_data) - return await fetch(METRICS_WRITE, { - method: "POST", - headers: { - "Content-Type": "text/plain", - }, - body: metrics_data, - }) - .then((data) => { - console.log(JSON.stringify(data)); - }) - .catch((e) => { - console.log(e); - }) - .finally(() => { - console.log("sent metrics"); - }); -}; - -const logs_push = async (logs_data: any) => { - await fetch(LOKI_WRITE, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - - body: logs_data, - }) - .then((response) => { - return response.json(); - }) - .catch((e) => console.log(e)) - .finally(() => { - console.log("web vitals sent"); - }); -}; - -const format_logs_queue = (queue: any) => { - - const mapped = Array.from(queue).map((entry: any) => ({ - stream: { - level: "info", - job: "webVitals", - name: entry.name, - value: entry.value, - rating: entry.rating, - delta: entry.delta, - }, - values: [ - [ - String(Date.now() * 1000000), - `job=WebVitals name=${entry.name} value=${entry.value} rating=${entry.rating} delta=${entry.delta} entries=${JSON.stringify(entry.entries)}`, - ], - ], - })); - if(Array.isArray(mapped) && mapped?.length > 0) { - return JSON.stringify({ streams: [...mapped] }); - } - return JSON.stringify({ streams: [] }); -}; - -const format_metrics_queue = (queue) => { - return Array.from(queue).map((entry: any) => ({ - metric: entry.name, - value: entry.value, - delta: entry.delta, - rating: entry.rating, - timestamp: Date.now() * 1_000_000, - })); -}; - -export async function flushQueue(queue) { - if (queue.size > 0) { - const logs_body = format_logs_queue(queue); - const queuearray = format_metrics_queue(queue); - await metrics_push(formatWebVitalsMetrics(queuearray)); - await logs_push(logs_body); - queue.clear(); - } -} - -export const handleVisibilityChange = async (queue) => { - if (document.visibilityState === "hidden") { - await flushQueue(queue); - } -}; - -export const reportWebVitals = (queue) => { - function addToQueue(metric) { - queue.add(metric); - } - onCLS(addToQueue); - onINP(addToQueue); - onFCP(addToQueue); - onTTFB(addToQueue); - onLCP(addToQueue); - - addEventListener( - "visibilitychange", - async () => await handleVisibilityChange(queue) - ); -}; diff --git a/packages/main/package.json b/packages/main/package.json index 62e8c093..4e2e6376 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -62,6 +62,7 @@ "slate-history": "^0.100.0", "slate-react": "^0.105.0", "tss-react": "^4.9.10", + "uuid": "^10.0.0", "web-vitals": "^4.2.3", "zod": "^3.23.8", "zustand": "^4.5.2" diff --git a/packages/main/plugins/PluginCards.tsx b/packages/main/plugins/PluginCards.tsx index 23a0c1dc..f75acc00 100644 --- a/packages/main/plugins/PluginCards.tsx +++ b/packages/main/plugins/PluginCards.tsx @@ -68,7 +68,7 @@ export const PluginCards: React.FC<{ const theme = useTheme(); return ( -
+
{filteredComponentList?.length > 0 && filteredComponentList?.map((component: any, k: number) => ( css` @@ -15,6 +15,7 @@ export const PluginPageStyles = (theme: QrynTheme) => css` flex: 1; overflow-x: hidden; display: flex; + flex-direction: column; flex: 1; height: 100%; overflow: hidden; @@ -25,6 +26,28 @@ export const PluginPageStyles = (theme: QrynTheme) => css` font-size: 14px; color: ${theme.contrast}; } + .page-header { + border-bottom: 1px solid ${theme.shadow}; + margin-bottom: 0.5em; + padding: 1em 0.25em; + display: flex; + gap: 0.25em; + align-items: baseline; + h1 { + font-size: 1.5em; + color: ${theme.contrast}; + } + h3 { + font-size: 1.25em; + color: ${theme.contrastNeutral}; + } + } + .cards-container { + display: flex; + flex-direction: row; + flex: 1; + gap: 1em; + } `; export const PluginCardStyles = (theme: QrynTheme) => css` @@ -77,4 +100,4 @@ export const InlineSwitch = css` display: flex; align-items: center; justify-content: space-between; -`; \ No newline at end of file +`; diff --git a/packages/main/plugins/Plugins.tsx b/packages/main/plugins/Plugins.tsx index a7c91f03..fe7556f4 100644 --- a/packages/main/plugins/Plugins.tsx +++ b/packages/main/plugins/Plugins.tsx @@ -4,11 +4,11 @@ import useTheme from "@ui/theme/useTheme"; import { LocalPluginsManagement } from "./PluginManagerFactory"; import { PluginPageStyles } from "./PluginStyles"; import { PluginCards } from "./PluginCards"; -import { useWebVitals } from "@util/useWebVitals"; +import { useWebVitals } from "@ui/plugins/WebVitals/useWebVitals"; export default function Plugins() { const theme = useTheme(); - useWebVitals({page:"Plugins"}); + useWebVitals({ page: "Plugins" }); const pl = LocalPluginsManagement(); const [local] = useState(pl.getAll()); const plugins: any = useMemo(() => { @@ -20,15 +20,22 @@ export default function Plugins() { return (
- {plugins?.length > 0 && - plugins?.map(([section, components]: any, index: number) => ( -
- -
- ))} +
+

Plugins

+

(need to reload page to activate)

+
+
+ {plugins?.length > 0 && + plugins?.map( + ([section, components]: any, index: number) => ( + + ) + )} +
); } diff --git a/packages/main/plugins/PluginsRenderer.tsx b/packages/main/plugins/PluginsRenderer.tsx index aea8e859..38a09f53 100644 --- a/packages/main/plugins/PluginsRenderer.tsx +++ b/packages/main/plugins/PluginsRenderer.tsx @@ -8,11 +8,18 @@ interface PluginRendererProps { const PluginRenderer: React.FC = (props) => { const { section, localProps } = props; - return ( -
+ return ( +
{PluginManager.getPlugins(section)?.length > 0 && PluginManager.getPlugins(section)?.map( - ( + ( { name, Component, diff --git a/packages/main/plugins/WebVitals/WebVitals.tsx b/packages/main/plugins/WebVitals/WebVitals.tsx new file mode 100644 index 00000000..c4ea3801 --- /dev/null +++ b/packages/main/plugins/WebVitals/WebVitals.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { WebVitalsStore } from "./store"; +import CustomSwitch from "@ui/qrynui/CustomSwitch/CustomSwitch"; +import { css, cx } from "@emotion/css"; +import useTheme from "@ui/theme/useTheme"; +import { QrynTheme } from "@ui/theme/types"; +import Tooltip from "@mui/material/Tooltip"; +const WebVitalStyles = (theme: QrynTheme) => css` + display: flex; + align-items: center; + p { + font-size: 0.75em; + color: ${theme.contrast}; + } +`; + +export const WebVitals: React.FC = () => { + const { active, setActive } = WebVitalsStore(); + const theme = useTheme(); + const onWebVitalsActivate = () => { + setActive(active); + }; + + return ( +
+ +

Monitor WebVitals

+
+ +
+ ); +}; diff --git a/packages/main/plugins/WebVitals/index.tsx b/packages/main/plugins/WebVitals/index.tsx new file mode 100644 index 00000000..cfc8f364 --- /dev/null +++ b/packages/main/plugins/WebVitals/index.tsx @@ -0,0 +1,15 @@ +import { Plugin } from "../types"; +import { nanoid } from "nanoid"; + +import { WebVitals } from "./WebVitals"; + +export const WebVitalsPlugin: Plugin = { + name: "Monitor Web Vitals", + section: "Status Bar", + id: nanoid(), + Component: WebVitals, + description: "Web vitals for qryn-view", + active: false, + visible: true, + roles: ["admin", "user", "guest", "superAdmin"], +}; diff --git a/packages/main/plugins/WebVitals/store.ts b/packages/main/plugins/WebVitals/store.ts new file mode 100644 index 00000000..cc9fa99b --- /dev/null +++ b/packages/main/plugins/WebVitals/store.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; + +export type WebVitalsStoreType = { + active: boolean; + setActive: (active: boolean) => void; +}; + +const initialState = { + active: + JSON.parse(localStorage.getItem("webVitalsActive") ?? "false") ?? false, +}; +export const WebVitalsStore = create((set) => ({ + ...initialState, + setActive: (active) => { + localStorage.setItem("webVitalsActive", JSON.stringify(!active)); + return set({ active: !active }); + }, +})); diff --git a/packages/main/plugins/WebVitals/useWebVitals.tsx b/packages/main/plugins/WebVitals/useWebVitals.tsx new file mode 100644 index 00000000..a027fec6 --- /dev/null +++ b/packages/main/plugins/WebVitals/useWebVitals.tsx @@ -0,0 +1,33 @@ +import { useEffect } from "react"; +import { + flushQueue, + handleVisibilityChange, + reportWebVitals, +} from "./webVitals"; +import { QueueItem } from "./webVitals"; +import { WebVitalsStore } from "./store"; + +export type WebVitalsProps = { + page?: string; +}; + +export const useWebVitals = ({ page }: WebVitalsProps) => { + const { active } = WebVitalsStore(); + + useEffect(() => { + // Queue Set + const metricsQueue = new Set(); + if (active) { + reportWebVitals(metricsQueue, page); + } + + return () => { + if (active) { + document.removeEventListener("visibilitychange", () => + handleVisibilityChange(metricsQueue) + ); + flushQueue(metricsQueue); + } + }; + }, []); +}; diff --git a/packages/main/plugins/WebVitals/webVitals.ts b/packages/main/plugins/WebVitals/webVitals.ts new file mode 100644 index 00000000..2679239c --- /dev/null +++ b/packages/main/plugins/WebVitals/webVitals.ts @@ -0,0 +1,403 @@ +import { onCLS, onFCP, onINP, onLCP, onTTFB, Metric } from "web-vitals"; +import { v4 as uuidv4 } from "uuid"; + +const location = window.location; +const url = location.protocol + "//" + location.host; +export const LOKI_WRITE = url + "/loki/api/v1/push"; +export const METRICS_WRITE = url + "/influx/api/v2/write"; +export const TEMPO_WRITE = url + "/tempo/api/push"; + +export const MetricDescription = { + CLS: "Cumulative Layout Shift", + FID: "First Input Delay", + FCP: "First Contentful Paint", + INP: "Interaction to Next Paint", + LCP: "Largest Contentful Paint", + TTFB: "Time to First Byte", +}; + +export interface QueueItem { + metric: Metric; + page: string; + traceId: string; +} + +const formatWebVitalsMetrics = (queue_array: QueueItem[]) => { + return queue_array + .map( + (item) => + `${item.metric.name},page=${item.page},rating=${item.metric.rating || "unknown"} value=${item.metric.value} ${Date.now() * 1000000}` + ) + .join("\n"); +}; + +const metrics_push = async (metrics_data: string): Promise => { + try { + await fetch(METRICS_WRITE, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: metrics_data, + }); + } catch (error) { + console.error("Failed to push metrics:", error); + throw error; + } +}; + +const logs_push = async (logs_data: string) => { + try { + await fetch(LOKI_WRITE, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: logs_data, + }); + } catch (error) { + console.error("Failed to push logs:", error); + throw error; + } +}; + +const format_logs_queue = (queue: QueueItem[]) => { + const mapped = queue.map(({ metric, page, traceId }) => ({ + stream: { + level: "info", + job: "webVitals", + name: metric.name, + description: + MetricDescription[ + metric.name as keyof typeof MetricDescription + ], + value: metric.value.toString(), + rating: metric.rating || "unknown", + delta: metric.delta?.toString() || "N/A", + traceId: traceId, + page: page, + }, + values: [ + [ + String(Date.now() * 1000000), + `job=WebVitals name=${metric.name} description=${MetricDescription[metric.name as keyof typeof MetricDescription]} traceId=${traceId} page=${page} value=${metric.value} rating=${metric.rating || "unknown"} delta=${metric.delta?.toString() || "N/A"} entries=${JSON.stringify(metric.entries || [])}`, + ], + ], + })); + return JSON.stringify({ streams: mapped }); +}; + +const format_metrics_queue = (queue: QueueItem[]): QueueItem[] => { + return queue.map(({ metric, page, traceId }) => ({ + metric, + page, + traceId, + })); +}; + +const createWebVitalSpan = (metric: any, page: string, traceId: string) => { + const timestamp = Math.floor(Date.now() * 1000); // microseconds + const parentId = uuidv4().replace(/-/g, ""); + + const createChildSpan = ( + name: string, + duration: number, + traceId: string, + parentId: string, + startOffset: number, + attributes?: Record + ) => { + return { + id: uuidv4().replace(/-/g, ""), + traceId: traceId, + parentId: parentId, + name: name, + timestamp: + Math.floor(Date.now() * 1000) + Math.floor(startOffset * 1000), + duration: Math.floor(duration * 1000), + tags: { + "web.vital.event": name, + "web.vital.page": page, + "web.vital.name": metric.name, + "web.vital.description": metric.description, + ...attributes, + }, + localEndpoint: { + serviceName: name, + }, + }; + }; + + const createTTFBParentSpan = (metric: any, page: string) => { + const timestamp = Math.floor(Date.now() * 1000); // microseconds + const entry: any = metric.entries?.[0]; + + return { + id: parentId, + traceId: traceId, + timestamp: timestamp, + duration: Math.floor(entry.duration * 1000), // microseconds + name: "TTFB", + tags: { + "http.method": "GET", + "http.path": page, + "web.vital.name": "TTFB", + "web.vital.description": metric.description, + "web.vital.value": metric.value.toString(), + "web.vital.rating": metric.rating || "unknown", + "web.vital.delta": metric.delta?.toString() || "N/A", + + // Additional TTFB-specific attributes + "ttfb.fetch_time": ( + entry.fetchStart - entry.navigationStart + ).toString(), + "ttfb.dns_time": ( + entry.domainLookupEnd - entry.domainLookupStart + ).toString(), + "ttfb.connect_time": ( + entry.connectEnd - entry.connectStart + ).toString(), + "ttfb.request_time": ( + entry.responseStart - entry.requestStart + ).toString(), + "ttfb.response_time": ( + entry.responseEnd - entry.responseStart + ).toString(), + + "http.response_content_length": + entry.encodedBodySize.toString(), + "http.response_transfer_size": entry.transferSize.toString(), + "http.response_decoded_content_length": + entry.decodedBodySize.toString(), + "http.status_code": entry.responseStatus.toString(), + "network.protocol.name": entry.nextHopProtocol || "unknown", + "network.protocol.version": entry.nextHopProtocol + ? entry.nextHopProtocol.split("/")[1] + : "unknown", + "ttfb.initiatorType": entry.initiatorType, + "ttfb.deliveryType": entry.deliveryType, + "ttfb.renderBlockingStatus": entry.renderBlockingStatus, + "ttfb.workerStart": entry.workerStart.toString(), + "ttfb.redirectCount": entry.redirectCount.toString(), + + // Additional performance metrics + "performance.domInteractive": entry.domInteractive.toString(), + "performance.domContentLoadedEvent": ( + entry.domContentLoadedEventEnd - + entry.domContentLoadedEventStart + ).toString(), + "performance.loadEvent": ( + entry.loadEventEnd - entry.loadEventStart + ).toString(), + }, + localEndpoint: { + serviceName: "Web Vitals", + }, + }; + }; + + let parentSpan: any = { + id: parentId, + traceId: traceId, + timestamp: timestamp, + duration: Math.floor(metric.value * 1000), // microseconds + name: metric.name, + tags: { + "http.method": "GET", + "http.path": page, + "web.vital.name": metric.name, + "web.vital.description": metric.description, + "web.vital.value": metric.value.toString(), + "web.vital.rating": metric.rating || "unknown", + "web.vital.delta": metric.delta?.toString() || "N/A", + }, + localEndpoint: { + serviceName: "Web Vitals", + }, + }; + + let childSpans = []; + + if (metric.name === "TTFB") { + parentSpan = createTTFBParentSpan(metric, page); + + const entry: any = metric.entries?.[0]; + if (entry) { + const baseTime = entry.startTime; + + childSpans = [ + createChildSpan( + "Navigation Start", + entry.startTime, + traceId, + parentId, + entry.startTime, + { "ttfb.startTime": entry.startTime } + ), + createChildSpan( + "Fetch Start", + entry.fetchStart, + traceId, + parentId, + entry.fetchStart, + { "ttfb.fetchStart": entry.fetchStart } + ), + createChildSpan( + "Domain Lookup", + entry.domainLookupEnd - entry.domainLookupStart, + traceId, + parentId, + entry.domainLookupStart - baseTime, + { + "ttfb.domainLookupStart": entry.domainLookupStart, + "ttfb.domainLookupEnd": entry.domainLookupEnd, + } + ), + createChildSpan( + "Connect", + entry.connectEnd - entry.connectStart, + traceId, + parentId, + entry.connectStart - baseTime, + { + "ttfb.connectStart": entry.connectStart, + "ttfb.connectEnd": entry.connectEnd, + } + ), + createChildSpan( + "Request/Response", + entry.responseEnd - entry.requestStart, + traceId, + parentId, + entry.requestStart - baseTime, + { + "ttfb.requestStart": entry.requestStart, + "ttfb.requestEnd": entry.requestEnd, + } + ), + createChildSpan( + "Unload Event", + entry.unloadEventEnd - entry.unloadEventStart, + traceId, + parentId, + entry.unloadEventStart - baseTime, + { + "ttfb.unloadEventStart": entry.unloadEventStart, + "ttfb.unloadEventEnd": entry.unloadEventEnd, + } + ), + createChildSpan( + "DOM Interactive", + 0, + traceId, + parentId, + entry.domInteractive - baseTime, + { + "ttfb.domInteractive": entry.domInteractive, + } + ), + createChildSpan( + "DOM Content Loaded", + entry.domContentLoadedEventEnd - + entry.domContentLoadedEventStart, + traceId, + parentId, + entry.domContentLoadedEventStart - baseTime, + { + "ttfb.domContentLoadedEventStart": + entry.domContentLoadedEventStart, + "ttfb.domContentLoadedEventEnd": + entry.domContentLoadedEventEnd, + } + ), + createChildSpan( + "DOM Complete", + 0, + traceId, + parentId, + entry.domComplete - baseTime, + { "ttfb.domComplete": entry.domComplete } + ), + createChildSpan( + "Load Event", + entry.loadEventEnd - entry.loadEventStart, + traceId, + parentId, + entry.loadEventStart - baseTime, + { + "ttfb.loadEventStart": entry.loadEventStart, + "ttfb.loadEventEnd": entry.loadEventEnd, + } + ), + ]; + } + } + + let spans = [parentSpan]; + + if (Object.keys(childSpans)?.length > 0) { + spans = [parentSpan, ...childSpans]; + } + + return spans; +}; + +const sendTraceData = async (spans: any[]) => { + try { + await fetch(TEMPO_WRITE, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(spans), + }); + } catch (error) { + console.error("Error sending trace data:", error); + } +}; + +export async function flushQueue(queue: Set) { + if (queue.size === 0) return; + + try { + const queueArray = Array.from(queue); + const logs_body = format_logs_queue(queueArray); + const metricsQueue = format_metrics_queue(queueArray); + const formattedMetrics = formatWebVitalsMetrics(metricsQueue); + const allSpans = queueArray.flatMap(({ metric, page, traceId }) => + createWebVitalSpan(metric, page, traceId) + ); + + await Promise.all([ + metrics_push(formattedMetrics), + logs_push(logs_body), + sendTraceData(allSpans), + ]); + + queue.clear(); + } catch (error) { + console.error("Error flushing queue:", error); + } +} + +export const handleVisibilityChange = async (queue: Set) => { + if (document.visibilityState === "hidden") { + await flushQueue(queue); + } +}; + +export const reportWebVitals = (queue: Set, page: string) => { + const addToQueue = async (metric: any) => { + const traceId = uuidv4().replace(/-/g, ""); + + queue.add({ metric, page, traceId }); + }; + + onCLS(addToQueue); + onFCP(addToQueue); + onINP(addToQueue); + onLCP(addToQueue); + onTTFB(addToQueue); + + addEventListener("visibilitychange", () => handleVisibilityChange(queue)); +}; diff --git a/packages/main/plugins/index.tsx b/packages/main/plugins/index.tsx index 838a3979..3f63966e 100644 --- a/packages/main/plugins/index.tsx +++ b/packages/main/plugins/index.tsx @@ -1,18 +1,19 @@ import { initPlugins, PluginManager } from "./PluginManagerFactory"; import clockPlugin from "./Clock"; -import { CardinalViewPlugin} from './Cardinality/' +import { CardinalViewPlugin } from "./Cardinality/"; +import { WebVitalsPlugin } from "./WebVitals"; //import raggixPlugin from "./Raggix"; //import aggregationPlugin from "./Aggregation" let plugins = [ clockPlugin, - // raggixPlugin, - // aggregationPlugin + // raggixPlugin, + // aggregationPlugin - CardinalViewPlugin - -] + CardinalViewPlugin, + WebVitalsPlugin, +]; -initPlugins(plugins) +initPlugins(plugins); export default PluginManager; diff --git a/packages/main/src/App.tsx b/packages/main/src/App.tsx index 5107fbc9..9f3d66e0 100644 --- a/packages/main/src/App.tsx +++ b/packages/main/src/App.tsx @@ -1,30 +1,32 @@ -import useTheme from "@ui/theme/useTheme"; +import useTheme from "@ui/theme/useTheme"; import { cx, css } from "@emotion/css"; import MainStatusBar from "@ui/views/Main/MainStatusBar"; -import { Outlet } from "react-router-dom"; +import { Outlet } from "react-router-dom"; import { Notification } from "@ui/qrynui/notifications"; import { useSelector } from "react-redux"; import SettingsDialog from "@ui/plugins/settingsdialog/SettingsDialog"; import { QrynTheme } from "@ui/theme/types"; -import { useWebVitals } from '../util/useWebVitals' -export const MainAppStyles = (theme:QrynTheme) => css` +import { useWebVitals } from "@ui/plugins/WebVitals/useWebVitals"; +export const MainAppStyles = (theme: QrynTheme) => css` background: ${theme.background}; - display:flex; - flex-direction:column; - height:100vh; - flex:1; + display: flex; + flex-direction: column; + height: 100vh; + flex: 1; `; export default function App() { const theme = useTheme(); - const settingDialogOpen = useSelector((store:any)=>store.settingsDialogOpen) - useWebVitals({page:"App"}) + const settingDialogOpen = useSelector( + (store: any) => store.settingsDialogOpen + ); + useWebVitals({ page: "App" }); return (
- + - +
); diff --git a/packages/main/tsconfig.json b/packages/main/tsconfig.json index 00c7583e..a8109272 100644 --- a/packages/main/tsconfig.json +++ b/packages/main/tsconfig.json @@ -37,6 +37,6 @@ "react": ["./node_modules/@types/react"], } }, - "include": ["src","views","util"], + "include": ["src","views","util", "plugins"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/packages/main/util/useWebVitals.tsx b/packages/main/util/useWebVitals.tsx deleted file mode 100644 index c6584cc1..00000000 --- a/packages/main/util/useWebVitals.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect } from "react"; -import {reportWebVitals, flushQueue,handleVisibilityChange } from './webVitals' - -export type WebVitalsProps = { - page?: string; -} - -export const useWebVitals = ({page}:WebVitalsProps) => { - useEffect(() => { - // Queue Set - const metricsQueue = new Set(); - reportWebVitals(metricsQueue, page) - return () => { - document.removeEventListener( - "visibilitychange", - handleVisibilityChange - ); - flushQueue(metricsQueue); - }; - }, []); -}; diff --git a/packages/main/util/webVitals.ts b/packages/main/util/webVitals.ts deleted file mode 100644 index aba136dc..00000000 --- a/packages/main/util/webVitals.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals"; - -const location = window.location; -const url = location.protocol + "//" + location.host; -export const LOKI_WRITE = url + "/loki/api/v1/push"; -export const METRICS_WRITE = url + "/influx/api/v2/write"; - -const formatWebVitalsMetrics = (queue_array) => { - return queue_array - .map( - (item) => - `${item.metric},level=info,page=${item.page},rating=${item.rating} value=${item.value} ${item.timestamp}` - ) - .join("\n"); -}; - -const metrics_push = async (metrics_data: any) => { - console.log(metrics_data); - return await fetch(METRICS_WRITE, { - method: "POST", - headers: { - "Content-Type": "text/plain", - }, - body: metrics_data, - }) - .then((data) => { - console.log(JSON.stringify(data)); - }) - .catch((e) => { - console.log(e); - }) - .finally(() => { - console.log("sent metrics"); - }); -}; - -const logs_push = async (logs_data: any) => { - await fetch(LOKI_WRITE, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - - body: logs_data, - }) - .then((response) => { - return response.json(); - }) - .catch((e) => console.log(e)) - .finally(() => { - console.log("web vitals sent"); - }); -}; -// add page name to entry -const format_logs_queue = (queue: any) => { - const mapped = Array.from(queue).map(({ metric, page }: any) => ({ - stream: { - level: "info", - job: "webVitals", - name: metric.name, - value: metric.value, - rating: metric.rating, - delta: metric.delta, - page: page ?? "", - }, - values: [ - [ - String(Date.now() * 1000000), - `job=WebVitals name=${metric.name} page=${page ?? ""} value=${metric.value} rating=${metric.rating} delta=${metric.delta} entries=${JSON.stringify(metric.entries)}`, - ], - ], - })); - if (Array.isArray(mapped) && mapped?.length > 0) { - return JSON.stringify({ streams: [...mapped] }); - } - return JSON.stringify({ streams: [] }); -}; -// add page name to entry -const format_metrics_queue = (queue) => { - return Array.from(queue).map(({ metric, page }: any) => ({ - metric: metric.name, - page: page ?? "", - value: metric.value, - delta: metric.delta, - rating: metric.rating, - timestamp: Date.now() * 1_000_000, - })); -}; - -export async function flushQueue(queue) { - if (queue.size > 0) { - const logs_body = format_logs_queue(queue); - const queuearray = format_metrics_queue(queue); - await metrics_push(formatWebVitalsMetrics(queuearray)); - await logs_push(logs_body); - queue.clear(); - } -} - -export const handleVisibilityChange = async (queue) => { - console.log(queue); - if (document.visibilityState === "hidden") { - await flushQueue(queue); - } -}; - -export const reportWebVitals = (queue, page) => { - console.log(queue, page); - function addToQueue(metric) { - queue.add(metric); - } - onCLS((metric) => addToQueue({ metric, page })); - onINP((metric) => addToQueue({ metric, page })); - onFCP((metric) => addToQueue({ metric, page })); - onTTFB((metric) => addToQueue({ metric, page })); - onLCP((metric) => addToQueue({ metric, page })); - - addEventListener( - "visibilitychange", - async () => await handleVisibilityChange(queue) - ); -}; diff --git a/packages/main/views/DataSources/DataSources.tsx b/packages/main/views/DataSources/DataSources.tsx index 9b1b2655..83036414 100644 --- a/packages/main/views/DataSources/DataSources.tsx +++ b/packages/main/views/DataSources/DataSources.tsx @@ -8,19 +8,8 @@ import { List } from "./views/List"; import { DataSourcesFiller } from "./components/DataSourcesFiller"; import { setTheme } from "@ui/store/actions"; import { useMediaQuery } from "react-responsive"; -import useTheme from "@ui/theme/useTheme" -import { useWebVitals} from '@util/useWebVitals' -export function getURlParams(params: any) { - const url = params.replace(/#/, ""); - const decoded = decodeURIComponent(url); - const urlParams = new URLSearchParams(decoded); - let panels = { left: "", right: "" }; - for (let [key, val] of urlParams) { - if (key === "left" || key === "right") { - panels[key] = JSON.parse(val); - } - } -} +import useTheme from "@ui/theme/useTheme"; +import { useWebVitals } from "@ui/plugins/WebVitals/useWebVitals"; export default function DataSources() { const isAutoDark = useMediaQuery({ query: "(prefers-color-scheme: dark)" }); @@ -28,8 +17,8 @@ export default function DataSources() { const theme = useTheme(); const autoTheme = useSelector((store: any) => store.autoTheme); - useWebVitals({page:"DataSources"}) - + useWebVitals({ page: "DataSources" }); + useEffect(() => { if (autoTheme) { const theme = isAutoDark ? "dark" : "light"; @@ -45,7 +34,7 @@ export default function DataSources() {
- +
css` background: ${theme.shadow}; @@ -26,8 +26,8 @@ export default function Main() { const navigate = useNavigate(); const dataSources = useSelector((store: any) => store.dataSources); - useWebVitals({page:"Main"}); - + useWebVitals({ page: "Main" }); + // get hash from current location const { hash } = useLocation(); // get url params as object @@ -36,8 +36,9 @@ export default function Main() { }, [hash]); UpdateStateFromQueryParams(); - // cookieAuth will be the cookie object - const { cookiesAvailable, cookieAuth, cookieUser } = useCookiesAvailable(paramsMemo); + // cookieAuth will be the cookie object + const { cookiesAvailable, cookieAuth, cookieUser } = + useCookiesAvailable(paramsMemo); const { urlAvailable, url } = useUrlAvailable(paramsMemo); @@ -60,25 +61,20 @@ export default function Main() { cookiesAvailable, dataSources ); - } else { updateDataSourcesFromLocalUrl(dataSources, dispatch, navigate); } - }, []); - useEffect(()=>{ - - if(cookieUser && typeof cookieUser === 'string') { + useEffect(() => { + if (cookieUser && typeof cookieUser === "string") { try { - dispatch(setCurrentUser(JSON.parse(cookieUser))) - - } catch(e) { - console.log(e) + dispatch(setCurrentUser(JSON.parse(cookieUser))); + } catch (e) { + console.log(e); } } - - },[cookieUser]) + }, [cookieUser]); useEffect(() => { const urlSetting = { @@ -113,7 +109,7 @@ export default function Main() { ); } }, [isAutoDark, autoTheme, dispatch]); - + const viewRenderer = ( isTabletOrMobile: boolean, isSplit: boolean, diff --git a/packages/main/views/Main/MainStatusBar.tsx b/packages/main/views/Main/MainStatusBar.tsx index d0293346..af3773c0 100644 --- a/packages/main/views/Main/MainStatusBar.tsx +++ b/packages/main/views/Main/MainStatusBar.tsx @@ -45,7 +45,6 @@ const StatusBarStyles = (theme: QrynTheme) => css` const MainStatusBar = () => { const theme = useTheme(); - return (
@@ -61,8 +60,9 @@ const MainStatusBar = () => {

- - +
+ +
); diff --git a/packages/main/vite.config.ts b/packages/main/vite.config.ts index 23b3cc27..d26a0756 100644 --- a/packages/main/vite.config.ts +++ b/packages/main/vite.config.ts @@ -97,6 +97,7 @@ let configOpts = { "@ui/theme": path.resolve(__dirname, "theme"), "@ui/qrynui": path.resolve(__dirname, "qrynui"), "@ui/environment": path.resolve(__dirname, "environment"), + "@util": path.resolve(__dirname, "util"), }, }, }; @@ -125,6 +126,11 @@ export default defineConfig(({ mode }) => { changeOrigin: env.VITE_API_BASE_URL, secure: false, }, + "/tempo": { + target: proxyApi, + changeOrigin: env.VITE_API_BASE_URL, + secure: false, + }, "/ready": { target: proxyApi, changeOrigin: env.VITE_API_BASE_URL, diff --git a/packages/main/workers/useVitalsWorker.tsx b/packages/main/workers/useVitalsWorker.tsx deleted file mode 100644 index 923a87bd..00000000 --- a/packages/main/workers/useVitalsWorker.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from "react"; -import {reportWebVitals, flushQueue,handleVisibilityChange } from '../helpers/webVitals' - -export const useWebVitals = () => { - useEffect(() => { - // Queue Set - const metricsQueue = new Set(); - reportWebVitals(metricsQueue) - return () => { - document.removeEventListener( - "visibilitychange", - handleVisibilityChange - ); - flushQueue(metricsQueue); - }; - }, []); -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fe8ad44..114f515d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: tss-react: specifier: ^4.9.10 version: 4.9.10(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.20(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + uuid: + specifier: ^10.0.0 + version: 10.0.0 web-vitals: specifier: ^4.2.3 version: 4.2.3 @@ -2883,6 +2886,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -5862,6 +5869,8 @@ snapshots: dependencies: react: 18.3.1 + uuid@10.0.0: {} + v8-compile-cache-lib@3.0.1: {} vite-node@1.6.0(@types/node@20.14.8)(sass@1.77.6): From 56c34362860cf111f6d3cb7c72ad40569d892cda Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Fri, 13 Sep 2024 18:24:22 +0200 Subject: [PATCH 04/12] fix: INP traces and initWebVitals --- packages/main/plugins/Plugins.tsx | 1 + .../WebVitals/{ => helper}/webVitals.ts | 165 ++++++++++++++---- .../main/plugins/WebVitals/initWebVitals.ts | 6 + packages/main/plugins/WebVitals/store.ts | 4 + .../main/plugins/WebVitals/useWebVitals.tsx | 4 +- 5 files changed, 142 insertions(+), 38 deletions(-) rename packages/main/plugins/WebVitals/{ => helper}/webVitals.ts (73%) create mode 100644 packages/main/plugins/WebVitals/initWebVitals.ts diff --git a/packages/main/plugins/Plugins.tsx b/packages/main/plugins/Plugins.tsx index fe7556f4..7c29b992 100644 --- a/packages/main/plugins/Plugins.tsx +++ b/packages/main/plugins/Plugins.tsx @@ -8,6 +8,7 @@ import { useWebVitals } from "@ui/plugins/WebVitals/useWebVitals"; export default function Plugins() { const theme = useTheme(); + // check web vitals useWebVitals({ page: "Plugins" }); const pl = LocalPluginsManagement(); const [local] = useState(pl.getAll()); diff --git a/packages/main/plugins/WebVitals/webVitals.ts b/packages/main/plugins/WebVitals/helper/webVitals.ts similarity index 73% rename from packages/main/plugins/WebVitals/webVitals.ts rename to packages/main/plugins/WebVitals/helper/webVitals.ts index 2679239c..d42a10f6 100644 --- a/packages/main/plugins/WebVitals/webVitals.ts +++ b/packages/main/plugins/WebVitals/helper/webVitals.ts @@ -1,6 +1,5 @@ import { onCLS, onFCP, onINP, onLCP, onTTFB, Metric } from "web-vitals"; import { v4 as uuidv4 } from "uuid"; - const location = window.location; const url = location.protocol + "//" + location.host; export const LOKI_WRITE = url + "/loki/api/v1/push"; @@ -22,6 +21,27 @@ export interface QueueItem { traceId: string; } +function simplifyINPArray(array: any[]): any[] { + const countMap = new Map(); + + // First pass: count occurrences + for (const entry of array) { + const key = JSON.stringify(entry); + countMap.set(key, (countMap.get(key) || 0) + 1); + } + + // Second pass: create result array + const result = []; + for (const [key, count] of countMap.entries()) { + const entry = JSON.parse(key); + const final_entry = { ...entry, count }; + + result.push(final_entry); + } + + return result; +} + const formatWebVitalsMetrics = (queue_array: QueueItem[]) => { return queue_array .map( @@ -61,30 +81,44 @@ const logs_push = async (logs_data: string) => { } }; -const format_logs_queue = (queue: QueueItem[]) => { - const mapped = queue.map(({ metric, page, traceId }) => ({ - stream: { - level: "info", - job: "webVitals", - name: metric.name, - description: - MetricDescription[ - metric.name as keyof typeof MetricDescription - ], - value: metric.value.toString(), - rating: metric.rating || "unknown", - delta: metric.delta?.toString() || "N/A", - traceId: traceId, - page: page, - }, - values: [ - [ - String(Date.now() * 1000000), - `job=WebVitals name=${metric.name} description=${MetricDescription[metric.name as keyof typeof MetricDescription]} traceId=${traceId} page=${page} value=${metric.value} rating=${metric.rating || "unknown"} delta=${metric.delta?.toString() || "N/A"} entries=${JSON.stringify(metric.entries || [])}`, - ], - ], - })); - return JSON.stringify({ streams: mapped }); +const format_logs_queue = async (queue: QueueItem[]) => { + // const encryption = new Encryption(); + // will format first the url with encoding to not show at request and spam everything + + const mapped = await queue.map(async ({ metric, page, traceId }) => { + let metric_entries: any = metric.entries; + + if (metric.name === "INP") { + metric_entries = simplifyINPArray(metric.entries); + } + + const logString = `job=WebVitals name=${metric.name} description="${MetricDescription[metric.name as keyof typeof MetricDescription]}" traceId=${traceId} page="${page}" value=${metric.value} rating=${metric.rating || "unknown"} delta=${metric.delta?.toString() || "N/A"} entries=${JSON.stringify(metric_entries || [])}`; + + return { + stream: { + level: "info", + job: "webVitals", + name: metric.name, + description: + MetricDescription[ + metric.name as keyof typeof MetricDescription + ], + value: metric.value.toString(), + rating: metric.rating || "unknown", + delta: metric.delta?.toString() || "N/A", + traceId: traceId, + page: page, + }, + values: [[String(Date.now() * 1000000), logString]], + }; + }); + + const streams_mapped = await Promise.all(mapped); + if (streams_mapped) { + return JSON.stringify({ streams: streams_mapped }); + } else { + return JSON.stringify({ streams: [] }); + } }; const format_metrics_queue = (queue: QueueItem[]): QueueItem[] => { @@ -196,27 +230,53 @@ const createWebVitalSpan = (metric: any, page: string, traceId: string) => { }; }; + const createINPParentSpan = (metric: any, page: string) => { + return { + id: parentId, + traceId: traceId, + timestamp: timestamp, + duration: Math.floor(metric.value * 1000), // microseconds + name: "INP", + tags: { + "http.method": "GET", + "inp.name": metric.name, + "inp.page": page, + "inp.value": metric.value, + "inp.rating": metric.rating, + "inp.delta": metric.delta, + }, + + localEndpoint: { + serviceName: "Web Vitals", + }, + }; + }; + let parentSpan: any = { id: parentId, traceId: traceId, timestamp: timestamp, duration: Math.floor(metric.value * 1000), // microseconds name: metric.name, - tags: { - "http.method": "GET", - "http.path": page, - "web.vital.name": metric.name, - "web.vital.description": metric.description, - "web.vital.value": metric.value.toString(), - "web.vital.rating": metric.rating || "unknown", - "web.vital.delta": metric.delta?.toString() || "N/A", - }, + localEndpoint: { serviceName: "Web Vitals", }, }; - let childSpans = []; + let tags: any = { + "http.method": "GET", + "http.path": page, + "web.vital.name": metric.name, + "web.vital.description": metric.description, + "web.vital.value": metric.value.toString(), + "web.vital.rating": metric.rating || "unknown", + "web.vital.delta": metric.delta?.toString() || "N/A", + }; + + parentSpan.tags = tags; + + let childSpans: any = []; if (metric.name === "TTFB") { parentSpan = createTTFBParentSpan(metric, page); @@ -333,6 +393,36 @@ const createWebVitalSpan = (metric: any, page: string, traceId: string) => { } } + if (metric.name === "INP") { + parentSpan = createINPParentSpan(metric, page); + const child_entries = simplifyINPArray(metric.entries)?.map( + (metric_entry) => + createChildSpan( + metric_entry.name, + Math.round( + metric_entry.processingEnd - + metric_entry.processingStart + ), + traceId, + parentId, + Math.round(metric_entry.processingStart), + { + "inp.name": metric_entry.name, + "inp.interactionId": metric_entry.interactionId, + "inp.startTime": metric_entry.startTime, + "inp.processingStart": metric_entry.processingStart, + "inp.processingEnd": metric_entry.processingEnd, + "inp.cancellable": metric_entry.cancelable, + "inp.count": metric_entry.count, + } + ) + ); + + if (child_entries?.length > 0) { + childSpans = child_entries; + } + } + let spans = [parentSpan]; if (Object.keys(childSpans)?.length > 0) { @@ -361,7 +451,10 @@ export async function flushQueue(queue: Set) { try { const queueArray = Array.from(queue); - const logs_body = format_logs_queue(queueArray); + const logs_body = await format_logs_queue(queueArray).then((data) => { + return data; + }); + const metricsQueue = format_metrics_queue(queueArray); const formattedMetrics = formatWebVitalsMetrics(metricsQueue); const allSpans = queueArray.flatMap(({ metric, page, traceId }) => diff --git a/packages/main/plugins/WebVitals/initWebVitals.ts b/packages/main/plugins/WebVitals/initWebVitals.ts new file mode 100644 index 00000000..6ae4a6e5 --- /dev/null +++ b/packages/main/plugins/WebVitals/initWebVitals.ts @@ -0,0 +1,6 @@ +import { WebVitalsStore } from './store'; + +export const initWebVitals = (apiUrl:string) => { + const {setApiUrl} = WebVitalsStore.getState(); + setApiUrl(apiUrl) +} \ No newline at end of file diff --git a/packages/main/plugins/WebVitals/store.ts b/packages/main/plugins/WebVitals/store.ts index cc9fa99b..d3b5b7f4 100644 --- a/packages/main/plugins/WebVitals/store.ts +++ b/packages/main/plugins/WebVitals/store.ts @@ -2,12 +2,15 @@ import { create } from "zustand"; export type WebVitalsStoreType = { active: boolean; + apiUrl: string | null; setActive: (active: boolean) => void; + setApiUrl: (apiUrl:string) => void; }; const initialState = { active: JSON.parse(localStorage.getItem("webVitalsActive") ?? "false") ?? false, + apiUrl: null, }; export const WebVitalsStore = create((set) => ({ ...initialState, @@ -15,4 +18,5 @@ export const WebVitalsStore = create((set) => ({ localStorage.setItem("webVitalsActive", JSON.stringify(!active)); return set({ active: !active }); }, + setApiUrl:(url) => set({apiUrl:url}), })); diff --git a/packages/main/plugins/WebVitals/useWebVitals.tsx b/packages/main/plugins/WebVitals/useWebVitals.tsx index a027fec6..2192f56e 100644 --- a/packages/main/plugins/WebVitals/useWebVitals.tsx +++ b/packages/main/plugins/WebVitals/useWebVitals.tsx @@ -3,8 +3,8 @@ import { flushQueue, handleVisibilityChange, reportWebVitals, -} from "./webVitals"; -import { QueueItem } from "./webVitals"; +} from "./helper/webVitals"; +import { QueueItem } from "./helper/webVitals"; import { WebVitalsStore } from "./store"; export type WebVitalsProps = { From bb3cadc0070d2dc166a920b84f6e010508410ba3 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Fri, 20 Sep 2024 17:30:49 +0200 Subject: [PATCH 05/12] feat: add metric charts --- packages/main/package.json | 1 + .../plugins/WebVitals/helper/webVitals.ts | 3 + .../plugins/WebVitals/performanceAxios.ts | 153 ++++++++++++++++++ packages/main/sections/Queries/QueryItem.tsx | 7 +- .../DataViews/components/Logs/LogRow.tsx | 10 +- .../DataViews/components/Logs/Row.tsx | 20 +-- .../DataViews/components/Logs/styled.tsx | 28 ++-- .../DataViews/components/Logs/types.tsx | 1 + .../components/ValueTags/MetricsChart.tsx | 102 ++++++++++++ .../components/ValueTags/ValueTags.tsx | 5 +- .../components/ValueTags/ValueTagsCont.tsx | 80 ++++++++- .../components/ValueTags/parseUrl.ts | 27 ++++ packages/main/store/actions/getData.ts | 47 ++++-- pnpm-lock.yaml | 23 +++ 14 files changed, 460 insertions(+), 47 deletions(-) create mode 100644 packages/main/plugins/WebVitals/performanceAxios.ts create mode 100644 packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx create mode 100644 packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts diff --git a/packages/main/package.json b/packages/main/package.json index 4e2e6376..2d83df61 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -30,6 +30,7 @@ "dayjs": "^1.11.12", "deep-freeze": "^0.0.1", "dnd-core": "^16.0.1", + "echarts": "^5.5.1", "fuzzy": "^0.1.3", "immutability-helper": "^3.1.1", "isomorphic-dompurify": "^1.13.0", diff --git a/packages/main/plugins/WebVitals/helper/webVitals.ts b/packages/main/plugins/WebVitals/helper/webVitals.ts index d42a10f6..3ae42217 100644 --- a/packages/main/plugins/WebVitals/helper/webVitals.ts +++ b/packages/main/plugins/WebVitals/helper/webVitals.ts @@ -99,6 +99,8 @@ const format_logs_queue = async (queue: QueueItem[]) => { level: "info", job: "webVitals", name: metric.name, + metricName: metric.name, + metricLabel: "page", description: MetricDescription[ metric.name as keyof typeof MetricDescription @@ -108,6 +110,7 @@ const format_logs_queue = async (queue: QueueItem[]) => { delta: metric.delta?.toString() || "N/A", traceId: traceId, page: page, + hasMetrics:true, }, values: [[String(Date.now() * 1000000), logString]], }; diff --git a/packages/main/plugins/WebVitals/performanceAxios.ts b/packages/main/plugins/WebVitals/performanceAxios.ts new file mode 100644 index 00000000..ac89d22b --- /dev/null +++ b/packages/main/plugins/WebVitals/performanceAxios.ts @@ -0,0 +1,153 @@ +import axios, { AxiosResponse } from "axios"; + +import { LOKI_WRITE, METRICS_WRITE, TEMPO_WRITE } from "./helper/webVitals"; +import { v4 as uuidv4 } from "uuid"; + +interface PerformanceEntry { + name: string; + startTime: number; + duration: number; + type?:string; + level?:string; + method: string; + url: string; + status: number; + traceId: string; +} + +const performanceQueue: Set = new Set(); + +export async function flushPerformanceQueue() { + if (performanceQueue.size === 0) return; + + const entries = Array.from(performanceQueue); + + // Format metrics + const metricsData = entries + .map( + (entry) => + `http_request,method=${entry.method},type=${entry.type},status=${entry.status} duration=${Math.round(entry.duration)} ${Date.now() * 1000000}`, + ) + .join("\n"); + + console.log(metricsData) + + // Format logs + const logsData = JSON.stringify({ + streams: entries.map((entry) => ({ + stream: { + level: entry.level ?? "info", + job: "httpRequest", + metricName:"http_request", + metricLabel:"type", + type: entry.type, + method: entry.method, + url: entry.url, + status: entry.status.toString(), + traceId: entry.traceId, + }, + values: [ + [ + String(Date.now() * 1000000), + `HTTP ${entry.method} ${entry.url} completed in ${entry.duration}ms with status ${entry.status}`, + ], + ], + })), + }); + + // Format traces + const tracesData = entries.map((entry) => ({ + id: uuidv4().replace(/-/g, ""), + traceId: entry.traceId, + name: `HTTP ${entry.type}`, + + timestamp:Math.floor(Date.now() * 1000), // microseconds + duration: Math.floor(entry.duration * 1000) , // microseconds + tags: { + "http.method": entry.method, + "http.url": entry.url, + "http.status_code": entry.status.toString(), + }, + localEndpoint: { + serviceName: "httpClient", + }, + })); + + console.log(tracesData) + + try { + await Promise.all([ + fetch(METRICS_WRITE, { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: metricsData, + }), + fetch(LOKI_WRITE, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: logsData, + }), + fetch(TEMPO_WRITE, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(tracesData), + }), + ]); + } catch (error) { + console.error("Error flushing performance queue:", error); + + } + + performanceQueue.clear(); +} + + + +export function performanceAxios( + config: any, +): Promise { + const startTime = performance.now(); + const traceId = uuidv4().replace(/-/g, ""); +console.log(config) + return axios(config) + .then((response) => { + const endTime = performance.now(); + const duration = endTime - startTime; + + const entry: PerformanceEntry = { + name: `${config.method} ${config.url}`, + type:config.type, + level:"info", + startTime, + duration, + method: config.method?.toUpperCase() || "GET", + url: config.url || "", + status: response.status, + traceId, + }; + + performanceQueue.add(entry); + + return response; + }) + .catch((error) => { + const endTime = performance.now(); + const duration = endTime - startTime; + + const entry: PerformanceEntry = { + name: `${config.method} ${config.url} (failed)`, + startTime, + duration, + type: config.type, + level:'error', + method: config.method?.toUpperCase() || "GET", + url: config.url || "", + status: error.response?.status || 0, + traceId, + }; + + performanceQueue.add(entry); + + throw error; + }); +} diff --git a/packages/main/sections/Queries/QueryItem.tsx b/packages/main/sections/Queries/QueryItem.tsx index b52124ea..95cc97cc 100644 --- a/packages/main/sections/Queries/QueryItem.tsx +++ b/packages/main/sections/Queries/QueryItem.tsx @@ -18,7 +18,7 @@ import { getStoredQueries, setStoredQuery, setLocalTabsState, - getLocalTabsState + getLocalTabsState, } from "./helpers"; import { useIdRefs } from "./hooks"; @@ -46,6 +46,8 @@ function TabPanel(props: TabPanelProps) { const QueryItem = (props: any) => { const { name, data } = props; + + console.log(data) const { id } = data; const [launchQuery, setLaunchQuery] = useState(""); const dispatch: any = useDispatch(); @@ -64,7 +66,6 @@ const QueryItem = (props: any) => { const deleteStoredQuery = (): void => { const prevStored = getStoredQueries(); - if (prevStored?.length > 0) { const filtered = filterLocal(prevStored, id); setStoredQuery(filtered); @@ -73,9 +74,7 @@ const QueryItem = (props: any) => { const onDeleteQuery = (): void => { const filtered = filterPanel(panelSelected, id); - const viewFiltered = filterPanel(dataView, id); - const prevStoredQuery = getStoredQueries(); if (prevStoredQuery?.length > 0) { diff --git a/packages/main/src/components/DataViews/components/Logs/LogRow.tsx b/packages/main/src/components/DataViews/components/Logs/LogRow.tsx index 4d6ba897..9b42fe70 100644 --- a/packages/main/src/components/DataViews/components/Logs/LogRow.tsx +++ b/packages/main/src/components/DataViews/components/Logs/LogRow.tsx @@ -1,11 +1,10 @@ - import { RowLogContent, RowTimestamp } from "./styled"; import { ILogRowProps } from "./types"; /** * Returns a Log Row with the row timestamp and text - * @param param0 - * @returns + * @param param0 + * @returns */ export function LogRow({ text, @@ -13,12 +12,13 @@ export function LogRow({ isMobile, isSplit, isShowTs, + onRowClick, }: ILogRowProps) { const showTimestamp = () => isShowTs && !isMobile && !isSplit; const dateFormatted = () => (isMobile || isSplit) && isShowTs; return ( -
+
{showTimestamp() && {dateFormated}} {dateFormatted() &&

{dateFormated}

} @@ -26,4 +26,4 @@ export function LogRow({
); -} \ No newline at end of file +} diff --git a/packages/main/src/components/DataViews/components/Logs/Row.tsx b/packages/main/src/components/DataViews/components/Logs/Row.tsx index 7581cdeb..f288be25 100644 --- a/packages/main/src/components/DataViews/components/Logs/Row.tsx +++ b/packages/main/src/components/DataViews/components/Logs/Row.tsx @@ -47,17 +47,17 @@ export function Row(props: IRowProps) { dataSourceData, }; - const rowProps = { - rowColor, - onClick: () => { - toggleItemActive(index); - }, - }; - return ( - - - + + toggleItemActive(index)} + {...logRowProps} + isShowTs={actQuery.isShowTs} + /> + toggleItemActive(index)} + {...valueTagsProps} + /> ); } diff --git a/packages/main/src/components/DataViews/components/Logs/styled.tsx b/packages/main/src/components/DataViews/components/Logs/styled.tsx index 820f07d0..958e44e8 100644 --- a/packages/main/src/components/DataViews/components/Logs/styled.tsx +++ b/packages/main/src/components/DataViews/components/Logs/styled.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; -import {css} from '@emotion/css'; +import { css } from "@emotion/css"; +import { type QrynTheme } from "@ui/theme/types"; export const FlexWrap = css` display: flex; @@ -7,9 +8,11 @@ export const FlexWrap = css` margin-top: 3px; `; - -export const LogRowStyled = styled.div` - color: ${({theme}: any) => theme.contrast}; +export const LogRowStyled: any = styled.div<{ + theme: QrynTheme; + rowColor: any; +}>` + color: ${({ theme }) => theme.contrast}; font-size: 12px; cursor: pointer; padding-left: 0.5rem; @@ -20,7 +23,7 @@ export const LogRowStyled = styled.div` margin-top: 2px; font-family: monospace; &:hover { - background: ${({theme}: any) => theme.activeBg}; + background: ${({ theme }: any) => theme.activeBg}; } p { @@ -28,21 +31,28 @@ export const LogRowStyled = styled.div` overflow-wrap: anywhere; margin-left: 3px; } - border-left: 4px solid ${({rowColor}: any) => rowColor}; + border-left: 4px solid ${({ rowColor }: any) => rowColor}; .log-ts-row { display: flex; } + .value-tags-close { + height: 12px; + &:hover { + background: ${({ theme }) => theme.shadow}; + border-radius: 3px 3px 0px 0px; + } + } `; export const RowLogContent = styled.span` font-size: 12px; - color: ${({theme}: any) => theme.hardContrast}; + color: ${({ theme }: any) => theme.hardContrast}; line-height: 1.5; `; export const RowTimestamp = styled.span` position: relative; - color: ${({theme}: any) => theme.contrast}; + color: ${({ theme }: any) => theme.contrast}; margin-right: 0.25rem; white-space: nowrap; font-size: 12px; @@ -54,5 +64,3 @@ export const RowsCont = styled.div` overflow-y: auto; height: calc(100% - 20px); `; - - diff --git a/packages/main/src/components/DataViews/components/Logs/types.tsx b/packages/main/src/components/DataViews/components/Logs/types.tsx index b488af33..a21523d2 100644 --- a/packages/main/src/components/DataViews/components/Logs/types.tsx +++ b/packages/main/src/components/DataViews/components/Logs/types.tsx @@ -4,6 +4,7 @@ export interface ILogRowProps { isSplit: boolean; isMobile: boolean; isShowTs: boolean; + onRowClick : () => void } export interface IRowProps { diff --git a/packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx b/packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx new file mode 100644 index 00000000..8b973456 --- /dev/null +++ b/packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx @@ -0,0 +1,102 @@ +import * as echarts from "echarts"; + +import React, { useRef, useEffect } from "react"; + +import dayjs from "dayjs"; + +type EChartsOption = echarts.EChartsOption; +export type MetricsChartProps = { + metricsData: any; + title: string; +}; + +export const MetricsChart: React.FC = ({ + metricsData, + title, +}) => { + const chartRef = useRef(null); + useEffect(() => { + if (metricsData?.length > 0 && chartRef.current) { + const myChart = echarts.init(chartRef.current); + + const formatMetricsData = (metricsData: any[]) => { + const series = []; + + for (let metric of metricsData) { + const entry = metric?.values?.reduce( + (acc, [date, data]) => { + acc.date.push( + dayjs(date * 1000).format("MM/DD HH:mm:ss") + ); + acc.data.push(data); + return acc; + }, + { date: [], data: [] } + ); + series.push(entry); + } + return series; + }; + + const series = formatMetricsData(metricsData); + const option: EChartsOption = { + width: chartRef.current.clientWidth - 85, + grid: { + left: 50, + }, + tooltip: { + trigger: "axis", + position: function (pt) { + return [pt[0], "10%"]; + }, + }, + title: { + left: "center", + text: title, + }, + toolbox: { + feature: { + dataZoom: { + yAxisIndex: "none", + }, + restore: {}, + saveAsImage: {}, + }, + }, + xAxis: { + type: "category", + boundaryGap: false, + data: series[0].date, + }, + yAxis: { + type: "value", + boundaryGap: [0, "100%"], + }, + dataZoom: [ + { + type: "inside", + start: 0, + end: 100, + }, + { + start: 0, + end: 100, + }, + ], + series: metricsData.map((metrics, index) => ({ + name: JSON.stringify(metrics.metric), + type: "line", + symbol: "none", + sampling: "none", + data: series[index].data, + })), + }; + + myChart.setOption(option); + } + }, [metricsData]); + + return ( +
+ ); +}; diff --git a/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx b/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx index d1436cba..be32ba8c 100644 --- a/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx +++ b/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx @@ -7,7 +7,7 @@ import { useMediaQuery } from "react-responsive"; import { LinkButtonWithTraces } from "./LinkButtonWithTraces"; import { ValueTagsStyled } from "./styled"; import { FilterButtons } from "./FilterButtons"; -import useTheme from "@ui/theme/useTheme"; +import useTheme from "@ui/theme/useTheme"; /** * @@ -19,7 +19,6 @@ import useTheme from "@ui/theme/useTheme"; * @returns Component for the Tags for the Log rows */ - export default function ValueTags(props: any) { const { tags, actQuery, dataSourceData, linkedFieldTags } = props; const isTabletOrMobile = useMediaQuery({ query: "(max-width: 1013px)" }); @@ -90,7 +89,7 @@ export default function ValueTags(props: any) { }; return ( - {Object.entries(tags).map(([label, value]:[string,string], k) => ( + {Object.entries(tags).map(([label, value]: [string, string], k) => (
{!isEmbed && ( diff --git a/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx b/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx index 9c158844..69ca62a2 100644 --- a/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx +++ b/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx @@ -1,18 +1,92 @@ import ValueTags from "./ValueTags"; +import { useState, useEffect } from "react"; +import { MetricsChart } from "./MetricsChart"; + /** - * - * @param props + * + * @param props * @returns Container for valueTags */ + +// this is the container for the labels inside each logs row + +export const KeyMetricLabels = { + webVitals: "page", + httpRequest: "type", +}; + +export const formatKeyLabels = (tags) => { + return { [KeyMetricLabels[tags.job]]: tags[KeyMetricLabels[tags.job]] }; +}; + export function ValueTagsCont(props: any) { - const { showLabels, tags, actQuery } = props; + const { onValueTagsClick, showLabels, tags, actQuery } = props; + const [logMetrics, setLogMetrics] = useState([]); + useEffect(() => { + if (tags?.metricName && tags?.metricLabel && showLabels) { + const url = constructRequestFromTags(tags.metricName, [ + tags.metricLabel, + tags[tags.metricLabel], + ]); + + const fetchMetrics = async () => + await fetch(url) + .then((data) => data.json()) + .then((res) => { + setLogMetrics(res.data.result); + }); + + fetchMetrics(); + } + }, [tags]); + + console.log(logMetrics, tags.hasMetrics); if (showLabels) { return (
+
+ {logMetrics?.length > 0 && + tags?.metricName && + tags?.metricLabel && ( + + )}
); } return null; } +// we could rebuild this with the relevant tags for each + +export function constructRequestFromTags(name, [key, val]) { + // Extract relevant properties from tags + //const { name, page } = tags; + + // Base URL + + const baseUrl =window.location.protocol + "//" + window.location.host + "/api/v1/query_range"; + + // Construct query parameter + const query = encodeURIComponent(`${name}{${key}="${val}"}`); + + // Time parameters + const end = Math.floor(Date.now() / 1000); // Current timestamp in seconds + const start = end - 24 * 60 * 60; // 24 hours ago + + // Other parameters + const limit = 100; + const step = 1; + const direction = "backwards"; + + // Construct the full URL + const url = `${baseUrl}?query=${query}&limit=${limit}&start=${start}&end=${end}&step=${step}&direction=${direction}`; + + return url; +} diff --git a/packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts b/packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts new file mode 100644 index 00000000..b089dd03 --- /dev/null +++ b/packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts @@ -0,0 +1,27 @@ +export function parseURL(url) { + // Extract the hash fragment + const hashParts = url.split("#"); + const hashFragment = hashParts[hashParts.length - 1]; + + // Split the hash fragment into key-value pairs + const pairs = hashFragment.split("&"); + const params = {}; + + pairs.forEach((pair) => { + const [key, value] = pair.split("="); + if (key && value) { + // Decode the value and handle array-like params + let decodedValue = decodeURIComponent(value); + if (key === "left" || key === "right") { + try { + decodedValue = JSON.parse(decodedValue); + } catch (e) { + console.error("Error parsing JSON for", key, e); + } + } + params[key] = decodedValue; + } + }); + + return params; +} diff --git a/packages/main/store/actions/getData.ts b/packages/main/store/actions/getData.ts index b14bfb09..8e391908 100644 --- a/packages/main/store/actions/getData.ts +++ b/packages/main/store/actions/getData.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import store from "@ui/store/store"; +import store from "@ui/store/store"; import setIsEmptyView from "./setIsEmptyView"; import { getEndpoint } from "./helpers/getEP"; import { getQueryOptions } from "./helpers/getQueryOptions"; @@ -15,6 +15,12 @@ import { DataViews } from "../store.model"; import { setLeftDataView } from "./setLeftDataView"; import { setRightDataView } from "./setRightDataView"; +import { + performanceAxios, + flushPerformanceQueue, +} from "@ui/plugins/WebVitals/performanceAxios"; +//import { flushQueue } from './webVitals'; + /** * * @param queryInput the expression text @@ -28,7 +34,7 @@ import { setRightDataView } from "./setRightDataView"; // this one should load logs and metrics data // just change endpoint -function panelDispatch(panel: string, dispatch: Function, data: any) { +function panelDispatch(panel: string, dispatch: any, data: any) { if (panel === "left") { return dispatch(setLeftPanel(data)); } @@ -40,7 +46,7 @@ function panelDispatch(panel: string, dispatch: Function, data: any) { export function dataViewDispatch( panel: string, dataViews: DataViews, - dispatch: Function + dispatch: any ) { if (panel === "left") { return dispatch(setLeftDataView(dataViews)); @@ -164,7 +170,7 @@ export default function getData( ); const endpoint = getEndpoint(type, queryType, params); - const setLoading = (state: boolean, dispatch: Function) => { + const setLoading = (state: boolean, dispatch: any) => { const dataViews: DataViews = store.getState()?.[`${panel}DataView`]; const dataView = dataViews?.find((view) => view.id === id); if (dataView) { @@ -172,7 +178,7 @@ export default function getData( } dataViewDispatch(panel, dataViews, dispatch); }; - return async function (dispatch: Function) { + return async function (dispatch: any) { setLoading(true, dispatch); loadingState(dispatch, true); let cancelToken: any; @@ -202,8 +208,15 @@ export default function getData( try { if (options?.method === "POST") { - await axios - ?.post(endpoint, queryInput, options) + //await axios + await performanceAxios({ + method: "POST", + url: endpoint, + data: queryInput, + type, + ...options, + }) + // ?.post(endpoint, queryInput, options) ?.then((response) => { processResponse( type, @@ -226,13 +239,22 @@ export default function getData( .finally(() => { setLoading(false, dispatch); loadingState(dispatch, false); + flushPerformanceQueue(); // Flush the performance data + // flushQueue(webVitalsQueue); }); } else if (options?.method === "GET") { - await axios - ?.get(endpoint, { - auth: { username: user, password: pass }, - ...options, - }) + // await axios + // ?.get(endpoint, { + // auth: { username: user, password: pass }, + // ...options, + // }) + await performanceAxios({ + method: "GET", + url: endpoint, + type, + auth: { username: user, password: pass }, + ...options, + }) ?.then((response) => { processResponse( type, @@ -273,6 +295,7 @@ export default function getData( .finally(() => { loadingState(dispatch, false); setLoading(false, dispatch); + flushPerformanceQueue(); // Flush the performance data }); } } catch (e) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 114f515d..7eddf6f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: dnd-core: specifier: ^16.0.1 version: 16.0.1 + echarts: + specifier: ^5.5.1 + version: 5.5.1 fuzzy: specifier: ^0.1.3 version: 0.1.3 @@ -1532,6 +1535,9 @@ packages: dompurify@3.1.5: resolution: {integrity: sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==} + echarts@5.5.1: + resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} + electron-to-chromium@1.4.806: resolution: {integrity: sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg==} @@ -2761,6 +2767,9 @@ packages: '@swc/wasm': optional: true + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tss-react@4.9.10: resolution: {integrity: sha512-uQj+r8mOKy0tv+/GAIzViVG81w/WeTCOF7tjsDyNjlicnWbxtssYwTvVjWT4lhWh5FSznDRy6RFp0BDdoLbxyg==} peerDependencies: @@ -3065,6 +3074,9 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zrender@5.6.0: + resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + zustand@4.5.2: resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} engines: {node: '>=12.7.0'} @@ -4438,6 +4450,11 @@ snapshots: dompurify@3.1.5: {} + echarts@5.5.1: + dependencies: + tslib: 2.3.0 + zrender: 5.6.0 + electron-to-chromium@1.4.806: {} entities@4.5.0: {} @@ -5769,6 +5786,8 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tslib@2.3.0: {} + tss-react@4.9.10(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.20(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@emotion/cache': 11.13.0 @@ -6019,6 +6038,10 @@ snapshots: zod@3.23.8: {} + zrender@5.6.0: + dependencies: + tslib: 2.3.0 + zustand@4.5.2(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1): dependencies: use-sync-external-store: 1.2.0(react@18.3.1) From 79837d0c75476047cba667467541f408ac5f6504 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Mon, 23 Sep 2024 13:54:02 +0200 Subject: [PATCH 06/12] fix: connect http logs with traces --- ...formanceAxios.ts => requestPerformance.ts} | 43 ++++++++++++------- packages/main/sections/Queries/QueryItem.tsx | 2 - .../components/ValueTags/ValueTagsCont.tsx | 1 - packages/main/store/actions/getData.ts | 8 ++-- 4 files changed, 32 insertions(+), 22 deletions(-) rename packages/main/plugins/WebVitals/{performanceAxios.ts => requestPerformance.ts} (76%) diff --git a/packages/main/plugins/WebVitals/performanceAxios.ts b/packages/main/plugins/WebVitals/requestPerformance.ts similarity index 76% rename from packages/main/plugins/WebVitals/performanceAxios.ts rename to packages/main/plugins/WebVitals/requestPerformance.ts index ac89d22b..0ef889e6 100644 --- a/packages/main/plugins/WebVitals/performanceAxios.ts +++ b/packages/main/plugins/WebVitals/requestPerformance.ts @@ -4,15 +4,18 @@ import { LOKI_WRITE, METRICS_WRITE, TEMPO_WRITE } from "./helper/webVitals"; import { v4 as uuidv4 } from "uuid"; interface PerformanceEntry { + id?:string; name: string; startTime: number; duration: number; + timestamp: number; type?:string; level?:string; method: string; url: string; status: number; - traceId: string; + value?: any; + traceId?: string; } const performanceQueue: Set = new Set(); @@ -26,12 +29,10 @@ export async function flushPerformanceQueue() { const metricsData = entries .map( (entry) => - `http_request,method=${entry.method},type=${entry.type},status=${entry.status} duration=${Math.round(entry.duration)} ${Date.now() * 1000000}`, + `http_request,method=${entry.method},type=${entry.type},status=${entry.status},level=${entry.level ?? "info"} duration=${Math.round(entry.duration)} ${entry.timestamp * 1000000}`, ) .join("\n"); - console.log(metricsData) - // Format logs const logsData = JSON.stringify({ streams: entries.map((entry) => ({ @@ -41,6 +42,7 @@ export async function flushPerformanceQueue() { metricName:"http_request", metricLabel:"type", type: entry.type, + duration: entry.duration, method: entry.method, url: entry.url, status: entry.status.toString(), @@ -48,8 +50,8 @@ export async function flushPerformanceQueue() { }, values: [ [ - String(Date.now() * 1000000), - `HTTP ${entry.method} ${entry.url} completed in ${entry.duration}ms with status ${entry.status}`, + String(String(entry.timestamp * 1000000)), + `job="httpRequest" type=${entry.type} HTTP ${entry.method} ${entry.url} traceId=${entry.traceId} completed in ${entry.duration}ms with status ${entry.status} and duration ${entry.duration}`, ], ], })), @@ -59,22 +61,21 @@ export async function flushPerformanceQueue() { const tracesData = entries.map((entry) => ({ id: uuidv4().replace(/-/g, ""), traceId: entry.traceId, - name: `HTTP ${entry.type}`, - - timestamp:Math.floor(Date.now() * 1000), // microseconds + name: `HTTP-${entry.type}`, + timestamp:Math.floor(entry.timestamp * 1000), // microseconds duration: Math.floor(entry.duration * 1000) , // microseconds + tags: { "http.method": entry.method, "http.url": entry.url, "http.status_code": entry.status.toString(), + "http.log_level": entry.level ?? "info", }, localEndpoint: { serviceName: "httpClient", }, })); - console.log(tracesData) - try { await Promise.all([ fetch(METRICS_WRITE, { @@ -103,27 +104,36 @@ export async function flushPerformanceQueue() { -export function performanceAxios( +export function requestPerformance( config: any, ): Promise { const startTime = performance.now(); - const traceId = uuidv4().replace(/-/g, ""); -console.log(config) + const timestamp = Date.now() + return axios(config) .then((response) => { + // filter in here if the interceptor is active const endTime = performance.now(); const duration = endTime - startTime; + + const traceId = uuidv4().replace(/-/g, ""); + + // add timestamp in here + + console.log(startTime) const entry: PerformanceEntry = { + traceId, name: `${config.method} ${config.url}`, type:config.type, + timestamp, level:"info", startTime, duration, method: config.method?.toUpperCase() || "GET", url: config.url || "", status: response.status, - traceId, + }; performanceQueue.add(entry); @@ -133,11 +143,14 @@ console.log(config) .catch((error) => { const endTime = performance.now(); const duration = endTime - startTime; + const timestamp = Date.now() + const traceId = uuidv4().replace(/-/g, ""); const entry: PerformanceEntry = { name: `${config.method} ${config.url} (failed)`, startTime, duration, + timestamp, type: config.type, level:'error', method: config.method?.toUpperCase() || "GET", diff --git a/packages/main/sections/Queries/QueryItem.tsx b/packages/main/sections/Queries/QueryItem.tsx index 95cc97cc..03b3a3d0 100644 --- a/packages/main/sections/Queries/QueryItem.tsx +++ b/packages/main/sections/Queries/QueryItem.tsx @@ -46,8 +46,6 @@ function TabPanel(props: TabPanelProps) { const QueryItem = (props: any) => { const { name, data } = props; - - console.log(data) const { id } = data; const [launchQuery, setLaunchQuery] = useState(""); const dispatch: any = useDispatch(); diff --git a/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx b/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx index 69ca62a2..0cf868b8 100644 --- a/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx +++ b/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx @@ -41,7 +41,6 @@ export function ValueTagsCont(props: any) { } }, [tags]); - console.log(logMetrics, tags.hasMetrics); if (showLabels) { return (
diff --git a/packages/main/store/actions/getData.ts b/packages/main/store/actions/getData.ts index 8e391908..b9d1fcc1 100644 --- a/packages/main/store/actions/getData.ts +++ b/packages/main/store/actions/getData.ts @@ -16,9 +16,9 @@ import { setLeftDataView } from "./setLeftDataView"; import { setRightDataView } from "./setRightDataView"; import { - performanceAxios, + requestPerformance, flushPerformanceQueue, -} from "@ui/plugins/WebVitals/performanceAxios"; +} from "@ui/plugins/WebVitals/requestPerformance"; //import { flushQueue } from './webVitals'; /** @@ -209,7 +209,7 @@ export default function getData( try { if (options?.method === "POST") { //await axios - await performanceAxios({ + await requestPerformance({ method: "POST", url: endpoint, data: queryInput, @@ -248,7 +248,7 @@ export default function getData( // auth: { username: user, password: pass }, // ...options, // }) - await performanceAxios({ + await requestPerformance({ method: "GET", url: endpoint, type, From 28fcac3b284b367665ab3f9e087d2b8a6be5d995 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Mon, 23 Sep 2024 18:43:24 +0200 Subject: [PATCH 07/12] fix: add instance fingerprint --- packages/main/package.json | 2 + packages/main/plugins/WebVitals/WebVitals.tsx | 26 +- .../plugins/WebVitals/helper/webVitals.ts | 31 +- .../plugins/WebVitals/requestPerformance.ts | 306 +++++++++--------- packages/main/plugins/WebVitals/store.ts | 34 +- .../main/plugins/WebVitals/useWebVitals.tsx | 6 +- packages/main/src/main.tsx | 32 +- pnpm-lock.yaml | 23 ++ 8 files changed, 277 insertions(+), 183 deletions(-) diff --git a/packages/main/package.json b/packages/main/package.json index 2d83df61..89105a58 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -14,6 +14,7 @@ "@emotion/is-prop-valid": "^1.3.0", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", + "@fingerprintjs/fingerprintjs": "^4.5.0", "@microlink/react-json-view": "latest", "@mui/base": "5.0.0-beta.5", "@mui/icons-material": "^5.15.20", @@ -71,6 +72,7 @@ "devDependencies": { "@emotion/babel-plugin": "^11.12.0", "@testing-library/react": "^14.3.1", + "@types/fingerprintjs": "^0.5.28", "@types/jest": "^29.5.12", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/packages/main/plugins/WebVitals/WebVitals.tsx b/packages/main/plugins/WebVitals/WebVitals.tsx index c4ea3801..be23c973 100644 --- a/packages/main/plugins/WebVitals/WebVitals.tsx +++ b/packages/main/plugins/WebVitals/WebVitals.tsx @@ -15,11 +15,21 @@ const WebVitalStyles = (theme: QrynTheme) => css` `; export const WebVitals: React.FC = () => { - const { active, setActive } = WebVitalsStore(); + const { + webVitalsActive, + httpPerformanceActive, + setWebVitalsActive, + setHttpPerformanceActive, + qrynInstance + } = WebVitalsStore(); + const theme = useTheme(); const onWebVitalsActivate = () => { - setActive(active); + setWebVitalsActive(webVitalsActive); }; + const onHttpPerformanceRequestActivate = () => { + setHttpPerformanceActive(httpPerformanceActive) + } return (
@@ -28,8 +38,18 @@ export const WebVitals: React.FC = () => { + +

Monitor HTTP Requests

+
+ + +

Instance: {qrynInstance ?? ""}

+
); }; diff --git a/packages/main/plugins/WebVitals/helper/webVitals.ts b/packages/main/plugins/WebVitals/helper/webVitals.ts index 3ae42217..d8e36f3b 100644 --- a/packages/main/plugins/WebVitals/helper/webVitals.ts +++ b/packages/main/plugins/WebVitals/helper/webVitals.ts @@ -1,5 +1,7 @@ import { onCLS, onFCP, onINP, onLCP, onTTFB, Metric } from "web-vitals"; import { v4 as uuidv4 } from "uuid"; +import { WebVitalsStore } from '@ui/plugins/WebVitals/store' + const location = window.location; const url = location.protocol + "//" + location.host; export const LOKI_WRITE = url + "/loki/api/v1/push"; @@ -19,6 +21,7 @@ export interface QueueItem { metric: Metric; page: string; traceId: string; + instance:string; } function simplifyINPArray(array: any[]): any[] { @@ -81,7 +84,7 @@ const logs_push = async (logs_data: string) => { } }; -const format_logs_queue = async (queue: QueueItem[]) => { +const format_logs_queue = async (queue: QueueItem[], instance: string) => { // const encryption = new Encryption(); // will format first the url with encoding to not show at request and spam everything @@ -92,7 +95,7 @@ const format_logs_queue = async (queue: QueueItem[]) => { metric_entries = simplifyINPArray(metric.entries); } - const logString = `job=WebVitals name=${metric.name} description="${MetricDescription[metric.name as keyof typeof MetricDescription]}" traceId=${traceId} page="${page}" value=${metric.value} rating=${metric.rating || "unknown"} delta=${metric.delta?.toString() || "N/A"} entries=${JSON.stringify(metric_entries || [])}`; + const logString = `job=WebVitals name=${metric.name} description="${MetricDescription[metric.name as keyof typeof MetricDescription]}" traceId=${traceId} instance=${instance} page="${page}" value=${metric.value} rating=${metric.rating || "unknown"} delta=${metric.delta?.toString() || "N/A"} entries=${JSON.stringify(metric_entries || [])}`; return { stream: { @@ -101,6 +104,7 @@ const format_logs_queue = async (queue: QueueItem[]) => { name: metric.name, metricName: metric.name, metricLabel: "page", + instance: instance, description: MetricDescription[ metric.name as keyof typeof MetricDescription @@ -124,15 +128,16 @@ const format_logs_queue = async (queue: QueueItem[]) => { } }; -const format_metrics_queue = (queue: QueueItem[]): QueueItem[] => { +const format_metrics_queue = (queue: QueueItem[], instance:string): QueueItem[] => { return queue.map(({ metric, page, traceId }) => ({ metric, page, traceId, + instance })); }; -const createWebVitalSpan = (metric: any, page: string, traceId: string) => { +const createWebVitalSpan = (metric: any, page: string, traceId: string, instance: string) => { const timestamp = Math.floor(Date.now() * 1000); // microseconds const parentId = uuidv4().replace(/-/g, ""); @@ -157,6 +162,7 @@ const createWebVitalSpan = (metric: any, page: string, traceId: string) => { "web.vital.page": page, "web.vital.name": metric.name, "web.vital.description": metric.description, + "qryn-view.instance": metric.instance, ...attributes, }, localEndpoint: { @@ -226,6 +232,7 @@ const createWebVitalSpan = (metric: any, page: string, traceId: string) => { "performance.loadEvent": ( entry.loadEventEnd - entry.loadEventStart ).toString(), + "qryn-view.instance" : instance, }, localEndpoint: { serviceName: "Web Vitals", @@ -247,6 +254,7 @@ const createWebVitalSpan = (metric: any, page: string, traceId: string) => { "inp.value": metric.value, "inp.rating": metric.rating, "inp.delta": metric.delta, + "qryn-view.instance": instance, }, localEndpoint: { @@ -275,6 +283,7 @@ const createWebVitalSpan = (metric: any, page: string, traceId: string) => { "web.vital.value": metric.value.toString(), "web.vital.rating": metric.rating || "unknown", "web.vital.delta": metric.delta?.toString() || "N/A", + "qryn-view.instance": instance }; parentSpan.tags = tags; @@ -417,6 +426,7 @@ const createWebVitalSpan = (metric: any, page: string, traceId: string) => { "inp.processingEnd": metric_entry.processingEnd, "inp.cancellable": metric_entry.cancelable, "inp.count": metric_entry.count, + "qryn-view.instance":instance, } ) ); @@ -450,18 +460,20 @@ const sendTraceData = async (spans: any[]) => { }; export async function flushQueue(queue: Set) { + if (queue.size === 0) return; try { + const {qrynInstance} = WebVitalsStore.getState() const queueArray = Array.from(queue); - const logs_body = await format_logs_queue(queueArray).then((data) => { + const logs_body = await format_logs_queue(queueArray,qrynInstance).then((data) => { return data; }); - const metricsQueue = format_metrics_queue(queueArray); + const metricsQueue = format_metrics_queue(queueArray, qrynInstance); const formattedMetrics = formatWebVitalsMetrics(metricsQueue); - const allSpans = queueArray.flatMap(({ metric, page, traceId }) => - createWebVitalSpan(metric, page, traceId) + const allSpans = queueArray.flatMap(({ metric, page, traceId, instance }) => + createWebVitalSpan(metric, page, traceId, instance) ); await Promise.all([ @@ -483,10 +495,11 @@ export const handleVisibilityChange = async (queue: Set) => { }; export const reportWebVitals = (queue: Set, page: string) => { + const { qrynInstance } = WebVitalsStore.getState(); const addToQueue = async (metric: any) => { const traceId = uuidv4().replace(/-/g, ""); - queue.add({ metric, page, traceId }); + queue.add({ metric, page, traceId, instance: qrynInstance}); }; onCLS(addToQueue); diff --git a/packages/main/plugins/WebVitals/requestPerformance.ts b/packages/main/plugins/WebVitals/requestPerformance.ts index 0ef889e6..6f3cb6f6 100644 --- a/packages/main/plugins/WebVitals/requestPerformance.ts +++ b/packages/main/plugins/WebVitals/requestPerformance.ts @@ -1,166 +1,172 @@ import axios, { AxiosResponse } from "axios"; - +import { WebVitalsStore } from "./store"; import { LOKI_WRITE, METRICS_WRITE, TEMPO_WRITE } from "./helper/webVitals"; import { v4 as uuidv4 } from "uuid"; interface PerformanceEntry { - id?:string; - name: string; - startTime: number; - duration: number; - timestamp: number; - type?:string; - level?:string; - method: string; - url: string; - status: number; - value?: any; - traceId?: string; + id?: string; + name: string; + startTime: number; + duration: number; + timestamp: number; + type?: string; + level?: string; + method: string; + url: string; + status: number; + value?: any; + traceId?: string; + instance?: string; } const performanceQueue: Set = new Set(); export async function flushPerformanceQueue() { - if (performanceQueue.size === 0) return; - - const entries = Array.from(performanceQueue); - - // Format metrics - const metricsData = entries - .map( - (entry) => - `http_request,method=${entry.method},type=${entry.type},status=${entry.status},level=${entry.level ?? "info"} duration=${Math.round(entry.duration)} ${entry.timestamp * 1000000}`, - ) - .join("\n"); - - // Format logs - const logsData = JSON.stringify({ - streams: entries.map((entry) => ({ - stream: { - level: entry.level ?? "info", - job: "httpRequest", - metricName:"http_request", - metricLabel:"type", - type: entry.type, - duration: entry.duration, - method: entry.method, - url: entry.url, - status: entry.status.toString(), - traceId: entry.traceId, - }, - values: [ - [ - String(String(entry.timestamp * 1000000)), - `job="httpRequest" type=${entry.type} HTTP ${entry.method} ${entry.url} traceId=${entry.traceId} completed in ${entry.duration}ms with status ${entry.status} and duration ${entry.duration}`, - ], - ], - })), - }); - - // Format traces - const tracesData = entries.map((entry) => ({ - id: uuidv4().replace(/-/g, ""), - traceId: entry.traceId, - name: `HTTP-${entry.type}`, - timestamp:Math.floor(entry.timestamp * 1000), // microseconds - duration: Math.floor(entry.duration * 1000) , // microseconds - - tags: { - "http.method": entry.method, - "http.url": entry.url, - "http.status_code": entry.status.toString(), - "http.log_level": entry.level ?? "info", - }, - localEndpoint: { - serviceName: "httpClient", - }, - })); - - try { - await Promise.all([ - fetch(METRICS_WRITE, { - method: "POST", - headers: { "Content-Type": "text/plain" }, - body: metricsData, - }), - fetch(LOKI_WRITE, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: logsData, - }), - fetch(TEMPO_WRITE, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(tracesData), - }), - ]); - } catch (error) { - console.error("Error flushing performance queue:", error); - - } + if (performanceQueue.size === 0) return; + + const entries = Array.from(performanceQueue); + + // Format metrics + const metricsData = entries + .map( + (entry) => + `http_request,method=${entry.method},instance=${entry.instance},type=${entry.type},status=${entry.status},level=${entry.level ?? "info"} duration=${Math.round(entry.duration)} ${entry.timestamp * 1000000}` + ) + .join("\n"); + + // Format logs + const logsData = JSON.stringify({ + streams: entries.map((entry) => ({ + stream: { + level: entry.level ?? "info", + job: "httpRequest", + metricName: "http_request", + metricLabel: "type", + type: entry.type, + duration: entry.duration, + method: entry.method, + url: entry.url, + status: entry.status.toString(), + traceId: entry.traceId, + instance: entry.instance, + }, + values: [ + [ + String(String(entry.timestamp * 1000000)), + `job="httpRequest" type=${entry.type} HTTP ${entry.method} ${entry.url} instance=${entry.instance} traceId=${entry.traceId} completed in ${entry.duration}ms with status ${entry.status} and duration ${entry.duration}`, + ], + ], + })), + }); - performanceQueue.clear(); + // Format traces + const tracesData = entries.map((entry) => ({ + id: uuidv4().replace(/-/g, ""), + traceId: entry.traceId, + name: `HTTP-${entry.type}`, + timestamp: Math.floor(entry.timestamp * 1000), // microseconds + duration: Math.floor(entry.duration * 1000), // microseconds + + tags: { + "http.method": entry.method, + "http.url": entry.url, + "http.status_code": entry.status.toString(), + "http.log_level": entry.level ?? "info", + "qryn-view.instance": entry.instance, + }, + localEndpoint: { + serviceName: "httpClient", + }, + })); + + try { + await Promise.all([ + fetch(METRICS_WRITE, { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: metricsData, + }), + fetch(LOKI_WRITE, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: logsData, + }), + fetch(TEMPO_WRITE, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(tracesData), + }), + ]); + } catch (error) { + console.error("Error flushing performance queue:", error); + } + + performanceQueue.clear(); } +export function requestPerformance(config: any): Promise { + const startTime = performance.now(); + const timestamp = Date.now(); + -export function requestPerformance( - config: any, -): Promise { - const startTime = performance.now(); - const timestamp = Date.now() - - return axios(config) - .then((response) => { - // filter in here if the interceptor is active - const endTime = performance.now(); - const duration = endTime - startTime; - - const traceId = uuidv4().replace(/-/g, ""); - - // add timestamp in here - - console.log(startTime) - - const entry: PerformanceEntry = { - traceId, - name: `${config.method} ${config.url}`, - type:config.type, - timestamp, - level:"info", - startTime, - duration, - method: config.method?.toUpperCase() || "GET", - url: config.url || "", - status: response.status, - - }; - - performanceQueue.add(entry); - - return response; - }) - .catch((error) => { - const endTime = performance.now(); - const duration = endTime - startTime; - const timestamp = Date.now() - const traceId = uuidv4().replace(/-/g, ""); - - const entry: PerformanceEntry = { - name: `${config.method} ${config.url} (failed)`, - startTime, - duration, - timestamp, - type: config.type, - level:'error', - method: config.method?.toUpperCase() || "GET", - url: config.url || "", - status: error.response?.status || 0, - traceId, - }; - - performanceQueue.add(entry); - - throw error; - }); + const { httpPerformanceActive, qrynInstance } = WebVitalsStore.getState(); + + const instance = qrynInstance + return axios(config) + .then((response) => { + // filter in here if the interceptor is active + const endTime = performance.now(); + const duration = endTime - startTime; + + const traceId = uuidv4().replace(/-/g, ""); + console.log(startTime); + + const entry: PerformanceEntry = { + traceId, + name: `${config.method} ${config.url}`, + type: config.type, + timestamp, + level: "info", + startTime, + instance, + duration, + method: config.method?.toUpperCase() || "GET", + url: config.url || "", + status: response.status, + }; + + if (httpPerformanceActive) { + performanceQueue.add(entry); + return response; + } + + return response; + }) + .catch((error) => { + const endTime = performance.now(); + const duration = endTime - startTime; + const timestamp = Date.now(); + const traceId = uuidv4().replace(/-/g, ""); + + const entry: PerformanceEntry = { + name: `${config.method} ${config.url} (failed)`, + startTime, + duration, + timestamp, + type: config.type, + instance, + level: "error", + method: config.method?.toUpperCase() || "GET", + url: config.url || "", + status: error.response?.status || 0, + traceId, + }; + if (httpPerformanceActive) { + performanceQueue.add(entry); + throw error; + } + + throw error; + }); } diff --git a/packages/main/plugins/WebVitals/store.ts b/packages/main/plugins/WebVitals/store.ts index d3b5b7f4..76d83bfb 100644 --- a/packages/main/plugins/WebVitals/store.ts +++ b/packages/main/plugins/WebVitals/store.ts @@ -1,22 +1,40 @@ import { create } from "zustand"; export type WebVitalsStoreType = { - active: boolean; + webVitalsActive: boolean; + httpPerformanceActive: boolean; apiUrl: string | null; - setActive: (active: boolean) => void; - setApiUrl: (apiUrl:string) => void; + setWebVitalsActive: (active: boolean) => void; + setHttpPerformanceActive: (active: boolean) => void; + setApiUrl: (apiUrl: string) => void; + qrynInstance: string; + setQrynInstance: (qrynInstance) => void; }; const initialState = { - active: + webVitalsActive: JSON.parse(localStorage.getItem("webVitalsActive") ?? "false") ?? false, - apiUrl: null, + httpPerformanceActive: + JSON.parse(localStorage.getItem("httpPerformanceActive") ?? "false") ?? + false, + qrynInstance: + JSON.parse(localStorage.getItem("qrynInstance") ?? "false") ?? false, + apiUrl: null, }; export const WebVitalsStore = create((set) => ({ ...initialState, - setActive: (active) => { + setWebVitalsActive: (active) => { localStorage.setItem("webVitalsActive", JSON.stringify(!active)); - return set({ active: !active }); + return set({ webVitalsActive: !active }); }, - setApiUrl:(url) => set({apiUrl:url}), + setHttpPerformanceActive: (active) => { + localStorage.setItem("httpPerformanceActive", JSON.stringify(!active)); + return set({ webVitalsActive: !active }); + }, + setQrynInstance: (qrynInstance) => { + localStorage.setItem("qrynInstance", JSON.stringify(qrynInstance)); + + return set({ qrynInstance }); + }, + setApiUrl: (url) => set({ apiUrl: url }), })); diff --git a/packages/main/plugins/WebVitals/useWebVitals.tsx b/packages/main/plugins/WebVitals/useWebVitals.tsx index 2192f56e..1570e4be 100644 --- a/packages/main/plugins/WebVitals/useWebVitals.tsx +++ b/packages/main/plugins/WebVitals/useWebVitals.tsx @@ -12,17 +12,17 @@ export type WebVitalsProps = { }; export const useWebVitals = ({ page }: WebVitalsProps) => { - const { active } = WebVitalsStore(); + const { webVitalsActive } = WebVitalsStore(); useEffect(() => { // Queue Set const metricsQueue = new Set(); - if (active) { + if (webVitalsActive) { reportWebVitals(metricsQueue, page); } return () => { - if (active) { + if (webVitalsActive) { document.removeEventListener("visibilitychange", () => handleVisibilityChange(metricsQueue) ); diff --git a/packages/main/src/main.tsx b/packages/main/src/main.tsx index 9a9ce70b..cc3ddb04 100644 --- a/packages/main/src/main.tsx +++ b/packages/main/src/main.tsx @@ -8,11 +8,7 @@ import errorInterceptor from "@ui/helpers/error.interceptor"; import { Notification } from "@ui/qrynui/notifications"; import { CookiesProvider } from "react-cookie"; -import { - Routes, - Route, - HashRouter, -} from "react-router-dom"; +import { Routes, Route, HashRouter } from "react-router-dom"; import { lazy, Suspense } from "react"; @@ -21,19 +17,35 @@ import ScreenLoader from "@ui/views/ScreenLoader"; import store from "@ui/store/store"; import { Provider } from "react-redux"; import ProtectedRoute from "./providers/ProtectedRoute"; +import * as fingerprintjs from "@fingerprintjs/fingerprintjs"; +import { WebVitalsStore } from "@ui/plugins/WebVitals/store"; -const AppRoute = lazy(() => import("./App")); +try { + const { webVitalsActive, httpPerformanceActive, setQrynInstance } = + WebVitalsStore.getState(); + if (webVitalsActive || httpPerformanceActive) { + const fpromise = async () => await fingerprintjs; + + fpromise() + .then((FingerprintJS) => FingerprintJS.load()) + ?.then((fp) => fp.get()) + ?.then(({ visitorId }) => { + // const { setQrynInstance } = WebVitalsStore.getState(); + setQrynInstance(visitorId); + }); + } +} catch (e) { + console.log(e); +} + +const AppRoute = lazy(() => import("./App")); const DataSourcesRoute = lazy( () => import("@ui/views/DataSources/DataSources") ); - const MainRoute = lazy(() => import("../views/Main")); - const PluginsRoute = lazy(() => import("../plugins/Plugins")); - const UserRoles = lazy(() => import("../views/User/UserRoles")); - errorInterceptor(axios); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7eddf6f8..94dcfa68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@emotion/styled': specifier: ^11.11.5 version: 11.11.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) + '@fingerprintjs/fingerprintjs': + specifier: ^4.5.0 + version: 4.5.0 '@microlink/react-json-view': specifier: latest version: 1.23.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -214,6 +217,9 @@ importers: '@testing-library/react': specifier: ^14.3.1 version: 14.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/fingerprintjs': + specifier: ^0.5.28 + version: 0.5.28 '@types/jest': specifier: ^29.5.12 version: 29.5.12 @@ -617,6 +623,9 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@fingerprintjs/fingerprintjs@4.5.0': + resolution: {integrity: sha512-mFSQoxyt8SGGRp1QUlhcnVtquW2HzCKfHKxAoIurR6soIJpuK3VvZuH0sg8eNaHH2dJhI3mZOEUx4k+P4GqXzw==} + '@floating-ui/core@1.6.2': resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==} @@ -1028,6 +1037,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/fingerprintjs@0.5.28': + resolution: {integrity: sha512-y4HGQNMocps8rj+S/2Q+qtVqI9XEE8ax2txSThPq9PHQq1ExLaShci31KAcLUtE+bczXEGeW8LzUqqwWlGk+KQ==} + '@types/hoist-non-react-statics@3.3.5': resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} @@ -2770,6 +2782,9 @@ packages: tslib@2.3.0: resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tss-react@4.9.10: resolution: {integrity: sha512-uQj+r8mOKy0tv+/GAIzViVG81w/WeTCOF7tjsDyNjlicnWbxtssYwTvVjWT4lhWh5FSznDRy6RFp0BDdoLbxyg==} peerDependencies: @@ -3459,6 +3474,10 @@ snapshots: '@eslint/js@8.57.0': {} + '@fingerprintjs/fingerprintjs@4.5.0': + dependencies: + tslib: 2.7.0 + '@floating-ui/core@1.6.2': dependencies: '@floating-ui/utils': 0.2.2 @@ -3878,6 +3897,8 @@ snapshots: '@types/estree@1.0.5': {} + '@types/fingerprintjs@0.5.28': {} + '@types/hoist-non-react-statics@3.3.5': dependencies: '@types/react': 18.3.3 @@ -5788,6 +5809,8 @@ snapshots: tslib@2.3.0: {} + tslib@2.7.0: {} + tss-react@4.9.10(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.20(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@emotion/cache': 11.13.0 From d4853812592369925abd5c32c1d6d1d881e65817 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Fri, 18 Oct 2024 18:33:15 +0200 Subject: [PATCH 08/12] feat: traces timerange lookup --- packages/main/plugins/QueryTest/index.tsx | 12 -- .../plugins/WebVitals/requestPerformance.ts | 18 +- packages/main/plugins/types.tsx | 2 +- .../main/qrynui/CustomSelect/CustomSelect.tsx | 186 ++++++++++++++++++ .../qrynui/CustomSelect/customSelectStyles.ts | 170 ++++++++++++++++ .../components/TraceSearch/TraceSearch.tsx | 97 ++++++++- packages/main/src/components/styles.ts | 4 +- packages/main/store/actions/getData.ts | 2 - .../store/actions/helpers/getTimeParams.ts | 25 ++- .../store/actions/helpers/processResponse.ts | 4 +- .../main/store/actions/setResponseType.ts | 2 +- packages/main/store/timeStore.ts | 20 ++ packages/main/views/Main/helpers.ts | 4 + 13 files changed, 498 insertions(+), 48 deletions(-) delete mode 100644 packages/main/plugins/QueryTest/index.tsx create mode 100644 packages/main/qrynui/CustomSelect/CustomSelect.tsx create mode 100644 packages/main/qrynui/CustomSelect/customSelectStyles.ts create mode 100644 packages/main/store/timeStore.ts diff --git a/packages/main/plugins/QueryTest/index.tsx b/packages/main/plugins/QueryTest/index.tsx deleted file mode 100644 index b251564e..00000000 --- a/packages/main/plugins/QueryTest/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import ReactJson from "react-json-view"; - -const QueryTest: React.FC = (props: any) => { - return ( - <> - - - ); -}; - -export default QueryTest; diff --git a/packages/main/plugins/WebVitals/requestPerformance.ts b/packages/main/plugins/WebVitals/requestPerformance.ts index 6f3cb6f6..741892e4 100644 --- a/packages/main/plugins/WebVitals/requestPerformance.ts +++ b/packages/main/plugins/WebVitals/requestPerformance.ts @@ -83,17 +83,25 @@ export async function flushPerformanceQueue() { await Promise.all([ fetch(METRICS_WRITE, { method: "POST", - headers: { "Content-Type": "text/plain" }, + headers: { + "Content-Type": "text/plain", + }, body: metricsData, }), fetch(LOKI_WRITE, { method: "POST", - headers: { "Content-Type": "application/json" }, + + headers: { + "Content-Type": "application/json", + }, body: logsData, }), fetch(TEMPO_WRITE, { method: "POST", - headers: { "Content-Type": "application/json" }, + + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify(tracesData), }), ]); @@ -108,11 +116,9 @@ export function requestPerformance(config: any): Promise { const startTime = performance.now(); const timestamp = Date.now(); - - const { httpPerformanceActive, qrynInstance } = WebVitalsStore.getState(); - const instance = qrynInstance + const instance = qrynInstance; return axios(config) .then((response) => { // filter in here if the interceptor is active diff --git a/packages/main/plugins/types.tsx b/packages/main/plugins/types.tsx index 87297a3a..53d84354 100644 --- a/packages/main/plugins/types.tsx +++ b/packages/main/plugins/types.tsx @@ -6,7 +6,7 @@ export interface Plugin { id: string; Component: React.FC; description: string; - visible: boolean; + visible?: boolean; active: boolean; roles: string[]; } diff --git a/packages/main/qrynui/CustomSelect/CustomSelect.tsx b/packages/main/qrynui/CustomSelect/CustomSelect.tsx new file mode 100644 index 00000000..f4d913dd --- /dev/null +++ b/packages/main/qrynui/CustomSelect/CustomSelect.tsx @@ -0,0 +1,186 @@ + +import React, { useEffect, useRef, useState } from "react"; +import { selectStyles } from './customSelectStyles' +import { cx } from '@emotion/css' +import useTheme from "@ui/theme/useTheme"; + +// Icon component +const Icon = ({ isOpen }) => { + return ( + + + + ); +}; + +// CloseIcon component +const CloseIcon = () => { + return ( + + + + ); +}; + +export type SelectValue = { + label:string + value: string +} + +export type SelectOption = SelectValue | SelectValue[] + +export type CustomSelectProps = { + placeHolder: string + options: SelectValue[] + defaultValue?: SelectOption + isMulti: boolean + isSearchable: boolean + onChange : (e:any) => void + align: string +} + +// CustomSelect component +export const CustomSelect = ({ placeHolder, options, isMulti, isSearchable, onChange, align, defaultValue = {label:"", value:""} }: CustomSelectProps) => { + // State variables using React hooks + const [showMenu, setShowMenu] = useState(false); // Controls the visibility of the dropdown menu + const [selectedValue, setSelectedValue] = useState(isMulti ? [defaultValue] : defaultValue); // Stores the selected value(s) + console.log(selectedValue) + const [searchValue, setSearchValue] = useState(""); // Stores the value entered in the search input + const searchRef = useRef(); // Reference to the search input element + const inputRef = useRef(); // Reference to the custom select input element + const theme = useTheme() + useEffect(() => { + setSearchValue(""); + if (showMenu && searchRef.current) { + searchRef.current.focus(); + } + }, [showMenu]); + + useEffect(() => { + const handler = (e) => { + if (inputRef.current && !inputRef.current.contains(e.target)) { + setShowMenu(false); + } + }; + + window.addEventListener("click", handler); + return () => { + window.removeEventListener("click", handler); + }; + }); + + const handleInputClick = () => { + setShowMenu(!showMenu); + }; + + const getDisplay = () => { + if (!selectedValue || selectedValue.length === 0) { + return placeHolder; + } + if (isMulti) { + return ( +
+ { + selectedValue.map((option, index) => ( +
+ {option.label} + onTagRemove(e, option)} className="dropdown-tag-close" > + + +
+ )) + } +
+ ); + } + return selectedValue.label; + }; + + const removeOption = (option) => { + return selectedValue.filter((o) => o.value !== option.value); + }; + + const onTagRemove = (e, option) => { + e.stopPropagation(); + const newValue = removeOption(option); + setSelectedValue(newValue); + onChange(newValue); + }; + + const onItemClick = (option) => { + let newValue; + if (isMulti) { + if (selectedValue.findIndex((o) => o.value === option.value) >= 0) { + newValue = removeOption(option); + } else { + newValue = [...selectedValue, option]; + } + } else { + newValue = option; + } + setSelectedValue(newValue); + onChange(newValue); + }; + + const isSelected = (option) => { + if (isMulti) { + return selectedValue.filter((o) => o.value === option.value).length > 0; + } + + if (!selectedValue) { + return false; + } + + return selectedValue.value === option.value; + }; + + const onSearch = (e) => { + setSearchValue(e.target.value); + }; + + const getOptions = () => { + if (!searchValue) { + return options; + } + + return options.filter( + (option) => + option.label.toLowerCase().indexOf(searchValue.toLowerCase()) >= 0 + ); + }; + + return ( +
+ +
+
{getDisplay()}
+
+
+ +
+
+
+ + { + showMenu && ( +
+ { + isSearchable && ( +
+ +
+ ) + } + { + getOptions().map((option) => ( +
onItemClick(option)} key={option.value} className={`dropdown-item ${isSelected(option) && "selected"}`} > + {option.label} +
+ )) + } +
+ ) + } +
+ ); +} \ No newline at end of file diff --git a/packages/main/qrynui/CustomSelect/customSelectStyles.ts b/packages/main/qrynui/CustomSelect/customSelectStyles.ts new file mode 100644 index 00000000..3eea735d --- /dev/null +++ b/packages/main/qrynui/CustomSelect/customSelectStyles.ts @@ -0,0 +1,170 @@ +import { css } from '@emotion/css' + +import { QrynTheme } from "@ui/theme/types"; + + + +export const selectStyles = (theme:QrynTheme) => css` + + text-align: left; + border: 1px solid ${theme.lightNeutral}; + background: ${theme.background}; + position: relative; + border-radius: 6px; + cursor: pointer; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + font-size:12px; + + + .dropdown-input { + padding: 4px 8px; + display: -webkit-box; + display: -ms-flexbox; + background: ${theme.shadow}; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + gap: 7px; +} + +.dropdown-input .dropdown-selected-value.placeholder { + color: ${theme.contrast}; +} + +.dropdown-tool svg { + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.dropdown-tool svg.translate { + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} + +.dropdown-menu { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + padding: 5px; + position: absolute; + -webkit-transform: translateY(6px); + -ms-transform: translateY(6px); + transform: translateY(6px); + border: 1px solid ${theme.shadow}; + border-radius: 6px; + overflow: auto; + background-color: ${theme.background}; + z-index: 99; + max-height: 312px; + min-height: 50px; +} + +.dropdown-menu::-webkit-scrollbar { + width: 5px; +} + +.dropdown-menu::-webkit-scrollbar-track { + background: ${theme.background}; +} + +.dropdown-menu::-webkit-scrollbar-thumb { + background: ${theme.shadow}; +} + +.dropdown-menu::-webkit-scrollbar-thumb:hover { + background: ${theme.shadow}; +} + +.dropdown-menu.alignment--left { + left: 0; +} + +.dropdown-menu.alignment--right { + right: 0; +} + +.dropdown-item { + padding: 7px 10px; + cursor: pointer; + + -webkit-transition: background-color 0.35s ease; + transition: background-color 0.35s ease; + border-radius: 6px; + font-weight: 500; +} + +.dropdown-item:hover { + background-color: ${theme.shadow}; + color: ${theme.contrast}; +} + +.dropdown-item.selected { + background-color: ${theme.shadow}; + color: ${theme.contrast}; + font-weight: 600; +} + +.search-box { + padding: 5px; +} + +.search-box input { + width: 100%; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 5px; + background: ${theme.background}; + border: 1px solid ${theme.shadow}; + border-radius: 5px; + :focus { + border-color: ${theme.shadow}; + color: ${theme.contrast}; + outline: none; + } +} + +.dropdown-tags { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 5px; +} + +.dropdown-tag-item { + background-color: #ff7300; + color: #FFF; + font-size: 12px; + font-weight: 500; + padding: 2px 4px; + border-radius: 6px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.dropdown-tag-close { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin-left: 5px; +} +` \ No newline at end of file diff --git a/packages/main/src/components/TraceSearch/TraceSearch.tsx b/packages/main/src/components/TraceSearch/TraceSearch.tsx index 2fe1ee74..6710758c 100644 --- a/packages/main/src/components/TraceSearch/TraceSearch.tsx +++ b/packages/main/src/components/TraceSearch/TraceSearch.tsx @@ -9,10 +9,38 @@ import { formatUrl } from "./tracesSearchUrl"; import { useTraceNames } from "./useTraceNames"; import { useTraceServiceName } from "./useTraceServiceName"; import Select, { components } from "react-select"; -import useTheme from "@ui/theme/useTheme" +import useTheme from "@ui/theme/useTheme"; import { cStyles } from "../DataViews/components/QueryBuilder/styles"; import { selectTheme } from "../DataViews/components/QueryBuilder/helpers"; import sanitizeWithSigns from "@ui/helpers/sanitizeWithSigns"; +import { SettingLabel } from "../styles"; +import CustomSwitch from "@ui/qrynui/CustomSwitch/CustomSwitch"; +import { timeStore, TimeState } from "@ui/store/timeStore"; +import { CustomSelect } from "@ui/qrynui/CustomSelect/CustomSelect"; +import { Tooltip } from "@mui/material"; + +export const timeRangeLabels = [ + "Last 5 minutes", + "Last 15 minutes", + "Last 30 minutes", + "Last 1 hour", + "Last 3 hours", + "Last 6 hours", + "Last 12 hours", + "Last 24 hours", + "Today", + "Yesterday", + "This Week", + "Last Week", + "Last 7 Days", +]; + +const selectFormatter = (opts: string[]) => { + return opts.map((option) => ({ + label: option, + value: option.split(" ").join("_"), + })); +}; const SearchColumn = css` display: flex; @@ -36,7 +64,6 @@ const TraceButton = (theme: any) => css` transition: 0.25s all; justify-content: center; padding: 3px 12px; - margin-top: 5px; height: 30px; display: flex; align-items: center; @@ -121,7 +148,7 @@ export const SearchSelect = forwardRef((props: any, ref: any) => { marginTop: "5px", display: "flex", alignItems: "center", - flex: 1, + flex: props?.isShort ? 0 : 1, whiteSpace: "nowrap", }} > @@ -159,6 +186,17 @@ export default function TracesSearch(props: any) { onSearchChange, } = props; + const { isTimeLookup, setIsTimeLookup, rangeLabel, setRangeLabel } = + timeStore.getState() as TimeState; + + const [options] = useState(selectFormatter(timeRangeLabels)); + + // default value for lookup: 1h + const [selectedLabel, setSelectedLabel] = useState({ + label: rangeLabel, + value: rangeLabel.split(" ").join("_"), + }); + const dispatch: any = useDispatch(); const serviceNameOpts = useTraceServiceName({ id: dataSourceId }); const traceNameOpts = useTraceNames({ id: dataSourceId }); @@ -167,11 +205,13 @@ export default function TracesSearch(props: any) { value: "", label: "", }); + const [spanValue, setSpanValue] = useState( traceNameOpts[0] || { name: "", value: "", label: "" } ); const theme = useTheme(); + const [urlState, setUrlState] = useState({ searchName: searchValue.value || "", name: spanValue.value || "", @@ -187,7 +227,6 @@ export default function TracesSearch(props: any) { if (serviceNameOpts.length > 0) { setUrlString(formatUrl(urlState)); } - }, [urlState]); const emit = () => { @@ -225,6 +264,10 @@ export default function TracesSearch(props: any) { onSearchChange(emit()); }; + const handleChangeSelect = (val: any) => { + setSelectedLabel(val); + setRangeLabel(val.label); + }; const onSubmit = () => { if (dataSourceURL && dataSourceURL !== "") { dispatch( @@ -293,10 +336,48 @@ export default function TracesSearch(props: any) { value={sanitizeWithSigns(urlState.maxDuration)} labelWidth={TRACE_SEARCH_LABEL_WIDTH} /> - - +
+
+ + Time Lookup + + setIsTimeLookup(!isTimeLookup)} + /> + handleChangeSelect(e)} + defaultValue={selectedLabel} + isSearchable + isMulti={false} + align="left" + /> +
+ +
); diff --git a/packages/main/src/components/styles.ts b/packages/main/src/components/styles.ts index bafd3c90..33f2e3d7 100644 --- a/packages/main/src/components/styles.ts +++ b/packages/main/src/components/styles.ts @@ -43,10 +43,10 @@ export const SettingsInputContainer = styled.div` `; -export const SettingLabel = styled.label` +export const SettingLabel = styled.div` font-size: 11px; white-space: nowrap; color: ${({ theme }:any) => theme.hardContrast}; - margin-left: 10px; + `; diff --git a/packages/main/store/actions/getData.ts b/packages/main/store/actions/getData.ts index b9d1fcc1..009a9345 100644 --- a/packages/main/store/actions/getData.ts +++ b/packages/main/store/actions/getData.ts @@ -168,7 +168,6 @@ export default function getData( id, panel ); - const endpoint = getEndpoint(type, queryType, params); const setLoading = (state: boolean, dispatch: any) => { const dataViews: DataViews = store.getState()?.[`${panel}DataView`]; @@ -208,7 +207,6 @@ export default function getData( try { if (options?.method === "POST") { - //await axios await requestPerformance({ method: "POST", url: endpoint, diff --git a/packages/main/store/actions/helpers/getTimeParams.ts b/packages/main/store/actions/helpers/getTimeParams.ts index 77ced25f..3f2ab1bc 100644 --- a/packages/main/store/actions/helpers/getTimeParams.ts +++ b/packages/main/store/actions/helpers/getTimeParams.ts @@ -1,17 +1,15 @@ +// sets the timerange for the getData action import { findRangeByLabel } from "@ui/main/components/StatusBar/components/daterangepicker/utils"; -import store from "@ui/store/store"; +import store from "@ui/store/store"; import { getTimeParsed, getTimeSec } from "./timeParser"; - +import { timeStore, TimeState } from "@ui/store/timeStore"; const getPrevTime = (lastTime: number) => { return lastTime || parseInt(new Date().getTime() + "000000"); }; - const getRangeByLabel = (rl: string, type?: string) => { let r: any = findRangeByLabel(rl); const { dateStart, dateEnd } = r; - let pStart, pStop; - if (type === "metrics") { pStart = getTimeSec(dateStart); pStop = getTimeSec(dateEnd); @@ -22,7 +20,6 @@ const getRangeByLabel = (rl: string, type?: string) => { pStart = parseInt(getTimeParsed(dateStart)); pStop = parseInt(getTimeParsed(dateEnd)); } - return { pStart, pStop, @@ -32,26 +29,26 @@ const getRangeByLabel = (rl: string, type?: string) => { }; export default function getTimeParams(type: string, id: string, panel: string) { const { time: lsTime, from, to } = store.getState(); - + const prevInstantTime = getPrevTime(lsTime); const queries = store.getState()[panel]; - const queryFound = queries?.find((f: any) => f.id === id); - const startTs = queryFound.start; const stopTs = queryFound.stop; - const prevInstantTime = getPrevTime(lsTime); - const rl: string = queryFound.label; + const { isTimeLookup, rangeLabel } = timeStore.getState() as TimeState; + const label = + type === "traces" && isTimeLookup && rangeLabel !== "" + ? rangeLabel + : rl; const _start = startTs; const _stop = stopTs; let parsedStart = 0, parsedStop = 0; - if (findRangeByLabel(rl)) { - const { pStart, pStop } = getRangeByLabel(rl, type); - + if (findRangeByLabel(label)) { + const { pStart, pStop } = getRangeByLabel(label, type); if (type === "traces") { parsedStart = Math.round(pStart / 1000000000); parsedStop = Math.round(pStop / 1000000000); diff --git a/packages/main/store/actions/helpers/processResponse.ts b/packages/main/store/actions/helpers/processResponse.ts index b94dc951..925a4048 100644 --- a/packages/main/store/actions/helpers/processResponse.ts +++ b/packages/main/store/actions/helpers/processResponse.ts @@ -19,7 +19,7 @@ export function setPanelData(panel: string, data: any) { export const resetTraceData = ( type: string, - dispatch: Function, + dispatch: any, panel: string, id: string, direction: QueryDirection, @@ -47,7 +47,7 @@ export const resetTraceData = ( export async function processResponse( type: string, response: any, - dispatch: Function, + dispatch: any, panel: string, id: string, direction: QueryDirection, diff --git a/packages/main/store/actions/setResponseType.ts b/packages/main/store/actions/setResponseType.ts index c8503e8c..35c891fe 100644 --- a/packages/main/store/actions/setResponseType.ts +++ b/packages/main/store/actions/setResponseType.ts @@ -1,5 +1,5 @@ export default function setResponseType(responseType: any) { - return function (dispatch: Function) { + return function (dispatch: any) { dispatch({ type: "SET_RESPONSE_TYPE", responseType diff --git a/packages/main/store/timeStore.ts b/packages/main/store/timeStore.ts new file mode 100644 index 00000000..494f4294 --- /dev/null +++ b/packages/main/store/timeStore.ts @@ -0,0 +1,20 @@ +import create from "zustand"; +import { persist } from "zustand/middleware"; +export type TimeState = { + rangeLabel: string; + isTimeLookup: boolean; + setRangeLabel: (rl: string) => void; + setIsTimeLookup: (tl: boolean) => void; +}; + +export const timeStore = create( + persist( + (set): TimeState => ({ + rangeLabel: "Last 1 hour", + isTimeLookup: true, + setRangeLabel: (rl: string) => set({ rangeLabel: rl }), + setIsTimeLookup: (tl: boolean) => set({ isTimeLookup: tl }), + }), + { name: "time-lookup" } + ) +); diff --git a/packages/main/views/Main/helpers.ts b/packages/main/views/Main/helpers.ts index e61cbbaa..9f36f04e 100644 --- a/packages/main/views/Main/helpers.ts +++ b/packages/main/views/Main/helpers.ts @@ -180,6 +180,8 @@ export async function checkLocalAPI( "Cardinality View", true ); + + // activate web vitals isReady = true; } else { @@ -194,6 +196,8 @@ export async function checkLocalAPI( "Cardinality View", false ) + + // deactivate web vitals isReady = true; } } From e47daeb046a215cf3fec74f282fe179efa3220d0 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Mon, 21 Oct 2024 17:35:53 +0200 Subject: [PATCH 09/12] fix: set default time range to last 5 minutes --- packages/main/helpers/stateFromQueryParams.ts | 3 +- .../QueryItem/QueryItemContainer.tsx | 9 +++- .../components/daterangepicker/index.tsx | 2 +- .../components/TraceSearch/TraceSearch.tsx | 54 +++---------------- packages/main/store/createInitialState.ts | 7 +-- packages/main/store/timeStore.ts | 4 +- 6 files changed, 23 insertions(+), 56 deletions(-) diff --git a/packages/main/helpers/stateFromQueryParams.ts b/packages/main/helpers/stateFromQueryParams.ts index 665d131b..d96ea117 100644 --- a/packages/main/helpers/stateFromQueryParams.ts +++ b/packages/main/helpers/stateFromQueryParams.ts @@ -3,6 +3,7 @@ import setDebug from "./setDebug"; import moment from "moment"; import { nanoid } from "nanoid"; import DOMPurify from "isomorphic-dompurify"; +import addMinutes from "date-fns/addMinutes"; const BOOLEAN_VALUES = ["isSubmit", "isSplit", "autoTheme", "isEmbed"]; export const initialUrlState: any = { query: "", @@ -66,7 +67,7 @@ export const initialUrlState: any = { values: [], // label name selected response: {}, // the target should be just the last one open: false, - start: new Date(Date.now() - 5 * 60000), + start: addMinutes(new Date(Date.now()), -5), time: "", // for instant queries stop: new Date(Date.now()), label: "", diff --git a/packages/main/src/components/QueryItem/QueryItemContainer.tsx b/packages/main/src/components/QueryItem/QueryItemContainer.tsx index 230a75e2..03c9bc39 100644 --- a/packages/main/src/components/QueryItem/QueryItemContainer.tsx +++ b/packages/main/src/components/QueryItem/QueryItemContainer.tsx @@ -58,6 +58,7 @@ export function QueryItemContainer(props: any) { const dataSources = useSelector((store: any) => store.dataSources); const isTabletOrMobile = useMediaQuery({ query: "(max-width: 1013px)" }); const [extValue, setExtValue] = useState(props.data.dataSourceId); + const [queryTimeLabel, setQueryTimeLabel] = useState (label !== "" ? label : "Last 5 minutes" ) const dataSourceOptions = useMemo(() => { if (dataSources.length > 0) { @@ -75,6 +76,12 @@ export function QueryItemContainer(props: any) { setExtValue(props.data.dataSourceId); }, []); + useEffect(()=>{ + if(label !== "") { + setQueryTimeLabel(label) + } + },[label]) + useEffect(() => { if (props?.data?.dataSourceId) { setExtValue(props.data.dataSourceId); @@ -301,7 +308,7 @@ export function QueryItemContainer(props: any) { onPickerOpen={onPickerOpen} startTs={start} stopTs={stop} - label={label} + label={queryTimeLabel} pickerOpen={pickerOpen} /> diff --git a/packages/main/src/components/StatusBar/components/daterangepicker/index.tsx b/packages/main/src/components/StatusBar/components/daterangepicker/index.tsx index 85956f19..94d6b85d 100644 --- a/packages/main/src/components/StatusBar/components/daterangepicker/index.tsx +++ b/packages/main/src/components/StatusBar/components/daterangepicker/index.tsx @@ -436,7 +436,7 @@ export function DateRangePickerMain(props: DateRangePickerProps) { - {label + {label !== "" ? label : (isValid(dateRange.dateStart) ? format( diff --git a/packages/main/src/components/TraceSearch/TraceSearch.tsx b/packages/main/src/components/TraceSearch/TraceSearch.tsx index 6710758c..93188605 100644 --- a/packages/main/src/components/TraceSearch/TraceSearch.tsx +++ b/packages/main/src/components/TraceSearch/TraceSearch.tsx @@ -13,11 +13,6 @@ import useTheme from "@ui/theme/useTheme"; import { cStyles } from "../DataViews/components/QueryBuilder/styles"; import { selectTheme } from "../DataViews/components/QueryBuilder/helpers"; import sanitizeWithSigns from "@ui/helpers/sanitizeWithSigns"; -import { SettingLabel } from "../styles"; -import CustomSwitch from "@ui/qrynui/CustomSwitch/CustomSwitch"; -import { timeStore, TimeState } from "@ui/store/timeStore"; -import { CustomSelect } from "@ui/qrynui/CustomSelect/CustomSelect"; -import { Tooltip } from "@mui/material"; export const timeRangeLabels = [ "Last 5 minutes", @@ -35,12 +30,6 @@ export const timeRangeLabels = [ "Last 7 Days", ]; -const selectFormatter = (opts: string[]) => { - return opts.map((option) => ({ - label: option, - value: option.split(" ").join("_"), - })); -}; const SearchColumn = css` display: flex; @@ -186,16 +175,9 @@ export default function TracesSearch(props: any) { onSearchChange, } = props; - const { isTimeLookup, setIsTimeLookup, rangeLabel, setRangeLabel } = - timeStore.getState() as TimeState; - const [options] = useState(selectFormatter(timeRangeLabels)); - // default value for lookup: 1h - const [selectedLabel, setSelectedLabel] = useState({ - label: rangeLabel, - value: rangeLabel.split(" ").join("_"), - }); + const dispatch: any = useDispatch(); const serviceNameOpts = useTraceServiceName({ id: dataSourceId }); @@ -264,10 +246,6 @@ export default function TracesSearch(props: any) { onSearchChange(emit()); }; - const handleChangeSelect = (val: any) => { - setSelectedLabel(val); - setRangeLabel(val.label); - }; const onSubmit = () => { if (dataSourceURL && dataSourceURL !== "") { dispatch( @@ -343,36 +321,16 @@ export default function TracesSearch(props: any) { gap: ".5em", marginTop: ".5em", padding: "1em", - justifyContent: "space-between", + justifyContent: "flex-end", background: theme.shadow, }} > -
- - Time Lookup - - setIsTimeLookup(!isTimeLookup)} - /> - handleChangeSelect(e)} - defaultValue={selectedLabel} - isSearchable - isMulti={false} - align="left" - /> -
+