mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-30 20:00:44 +01:00
Compare commits
1 Commits
nv/dashboa
...
refactor/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ed9e520f1 |
7
.github/workflows/build-enterprise.yaml
vendored
7
.github/workflows/build-enterprise.yaml
vendored
@@ -61,13 +61,6 @@ jobs:
|
||||
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
|
||||
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
|
||||
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
|
||||
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
|
||||
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
||||
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
|
||||
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
|
||||
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
|
||||
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
|
||||
echo 'VITE_ENVIRONMENT="production"' >> frontend/.env
|
||||
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env
|
||||
|
||||
6
.github/workflows/build-staging.yaml
vendored
6
.github/workflows/build-staging.yaml
vendored
@@ -67,12 +67,6 @@ jobs:
|
||||
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
|
||||
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
|
||||
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
|
||||
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
|
||||
echo 'VITE_TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
|
||||
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||
echo 'VITE_PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
|
||||
echo 'VITE_APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
|
||||
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
|
||||
echo 'VITE_DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
|
||||
echo 'VITE_ENVIRONMENT="staging"' >> frontend/.env
|
||||
echo 'VITE_VERSION="${{ steps.build-info.outputs.version }}"' >> frontend/.env
|
||||
|
||||
7
.github/workflows/gor-signoz.yaml
vendored
7
.github/workflows/gor-signoz.yaml
vendored
@@ -27,13 +27,6 @@ jobs:
|
||||
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
|
||||
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
|
||||
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
|
||||
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
|
||||
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
|
||||
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
|
||||
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
|
||||
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
|
||||
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
|
||||
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
|
||||
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> .env
|
||||
echo 'VITE_ENVIRONMENT="production"' >> .env
|
||||
echo 'VITE_VERSION="${{ github.ref_name }}"' >> .env
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
NODE_ENV="development"
|
||||
BUNDLE_ANALYSER="true"
|
||||
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
|
||||
VITE_PYLON_APP_ID="pylon-app-id"
|
||||
VITE_APPCUES_APP_ID="appcess-app-id"
|
||||
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
|
||||
|
||||
CI="1"
|
||||
|
||||
# API
|
||||
VITE_BASE_PATH=""
|
||||
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
|
||||
VITE_WEBSOCKET_API_ENDPOINT=""
|
||||
|
||||
# Pylon
|
||||
VITE_PYLON_ENABLED="false"
|
||||
VITE_PYLON_APP_ID=""
|
||||
VITE_PYLON_IDENTITY_SECRET=""
|
||||
|
||||
# Appcues
|
||||
VITE_APPCUES_ENABLED="false"
|
||||
VITE_APPCUES_APP_ID=""
|
||||
|
||||
# PostHog
|
||||
VITE_POSTHOG_ENABLED="false"
|
||||
VITE_POSTHOG_API_HOST=""
|
||||
VITE_POSTHOG_KEY=""
|
||||
VITE_POSTHOG_UI_HOST=""
|
||||
|
||||
# Sentry
|
||||
VITE_SENTRY_ENABLED="false"
|
||||
VITE_SENTRY_AUTH_TOKEN=""
|
||||
VITE_SENTRY_ORG=""
|
||||
VITE_SENTRY_PROJECT_ID=""
|
||||
VITE_SENTRY_TUNNEL=""
|
||||
VITE_SENTRY_DSN=""
|
||||
|
||||
# Docs
|
||||
VITE_DOCS_BASE_URL="https://signoz.io"
|
||||
|
||||
# Build info
|
||||
VITE_ENVIRONMENT="development"
|
||||
VITE_VERSION=""
|
||||
|
||||
@@ -111,11 +111,10 @@
|
||||
<div id="root"></div>
|
||||
|
||||
<script>
|
||||
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
|
||||
var pylonSettings =
|
||||
((window.signozBootData || {}).settings || {}).pylon || {};
|
||||
var pylonEnabled = pylonSettings.enabled !== false;
|
||||
if (PYLON_APP_ID && pylonEnabled) {
|
||||
var pylonEnabled = pylonSettings.enabled === true;
|
||||
if (pylonSettings.appId && pylonEnabled) {
|
||||
(function () {
|
||||
var e = window;
|
||||
var t = document;
|
||||
@@ -133,7 +132,7 @@
|
||||
e.setAttribute('async', 'true');
|
||||
e.setAttribute(
|
||||
'src',
|
||||
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
|
||||
'https://widget.usepylon.com/widget/' + pylonSettings.appId,
|
||||
);
|
||||
var n = t.getElementsByTagName('script')[0];
|
||||
n.parentNode.insertBefore(e, n);
|
||||
@@ -150,15 +149,14 @@
|
||||
window.AppcuesSettings = { enableURLDetection: true };
|
||||
</script>
|
||||
<script>
|
||||
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
|
||||
var appcuesSettings =
|
||||
((window.signozBootData || {}).settings || {}).appcues || {};
|
||||
var appcuesEnabled = appcuesSettings.enabled !== false;
|
||||
if (APPCUES_APP_ID && appcuesEnabled) {
|
||||
var appcuesEnabled = appcuesSettings.enabled === true;
|
||||
if (appcuesSettings.appId && appcuesEnabled) {
|
||||
(function (d, t) {
|
||||
var a = d.createElement(t);
|
||||
a.async = 1;
|
||||
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
|
||||
a.src = '//fast.appcues.com/' + appcuesSettings.appId + '.js';
|
||||
var s = d.getElementsByTagName(t)[0];
|
||||
s.parentNode.insertBefore(a, s);
|
||||
})(document, 'script');
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"project": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"ignore": ["src/api/generated/**/*.ts", "src/typings/*.ts"],
|
||||
"ignoreDependencies": [
|
||||
"http-proxy-middleware",
|
||||
"@typescript/native-preview"
|
||||
]
|
||||
}
|
||||
@@ -79,7 +79,6 @@
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"eventemitter3": "5.0.1",
|
||||
"history": "4.10.1",
|
||||
"http-proxy-middleware": "4.1.1",
|
||||
"http-status-codes": "2.3.0",
|
||||
"i18next": "^21.6.12",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
|
||||
22
frontend/pnpm-lock.yaml
generated
22
frontend/pnpm-lock.yaml
generated
@@ -164,9 +164,6 @@ importers:
|
||||
history:
|
||||
specifier: 4.10.1
|
||||
version: 4.10.1
|
||||
http-proxy-middleware:
|
||||
specifier: 4.1.1
|
||||
version: 4.1.1
|
||||
http-status-codes:
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
@@ -5461,10 +5458,6 @@ packages:
|
||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
http-proxy-middleware@4.1.1:
|
||||
resolution: {integrity: sha512-KX5ZofGXLFXqFAkQoOWZ+rTtaLTut7m0gyL+QzJrdejtIZ+F4bPPDoe7reISg2+v0CAz5OfVwEJEhty7X+e57g==}
|
||||
engines: {node: ^22.15.0 || ^24.0.0 || >=26.0.0}
|
||||
|
||||
http-status-codes@2.3.0:
|
||||
resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==}
|
||||
|
||||
@@ -5472,9 +5465,6 @@ packages:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
httpxy@0.5.3:
|
||||
resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -14515,16 +14505,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy-middleware@4.1.1:
|
||||
dependencies:
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
httpxy: 0.5.3
|
||||
is-glob: 4.0.3
|
||||
is-plain-obj: 4.1.0
|
||||
micromatch: 4.0.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-status-codes@2.3.0: {}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
@@ -14534,8 +14514,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
httpxy@0.5.3: {}
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
human-signals@8.0.1: {}
|
||||
|
||||
@@ -292,10 +292,10 @@ function App(): JSX.Element {
|
||||
isChatSupportEnabled &&
|
||||
!showAddCreditCardModal &&
|
||||
(isCloudUser || isEnterpriseSelfHostedUser) &&
|
||||
(window.signozBootData?.settings?.pylon.enabled ?? true)
|
||||
window.signozBootData?.settings?.pylon?.enabled
|
||||
) {
|
||||
const email = user.email || '';
|
||||
const secret = process.env.PYLON_IDENTITY_SECRET || '';
|
||||
const secret = window.signozBootData?.settings?.pylon?.identitySecret || '';
|
||||
let emailHash = '';
|
||||
|
||||
if (email && secret) {
|
||||
@@ -304,7 +304,7 @@ function App(): JSX.Element {
|
||||
|
||||
window.pylon = {
|
||||
chat_settings: {
|
||||
app_id: process.env.PYLON_APP_ID,
|
||||
app_id: window.signozBootData?.settings?.pylon?.appId,
|
||||
email: user.email,
|
||||
name: user.displayName || user.email,
|
||||
email_hash: emailHash,
|
||||
@@ -335,22 +335,23 @@ function App(): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (isCloudUser || isEnterpriseSelfHostedUser) {
|
||||
if (
|
||||
(window.signozBootData?.settings?.posthog.enabled ?? true) &&
|
||||
process.env.POSTHOG_KEY
|
||||
window.signozBootData?.settings?.posthog?.enabled &&
|
||||
window.signozBootData?.settings?.posthog?.key
|
||||
) {
|
||||
posthog.init(process.env.POSTHOG_KEY, {
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
posthog.init(window.signozBootData.settings.posthog.key, {
|
||||
api_host: window.signozBootData.settings.posthog.apiHost,
|
||||
ui_host: window.signozBootData.settings.posthog.uiHost,
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!isSentryInitialized &&
|
||||
(window.signozBootData?.settings?.sentry.enabled ?? true)
|
||||
window.signozBootData?.settings?.sentry?.enabled
|
||||
) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
tunnel: process.env.TUNNEL_URL,
|
||||
dsn: window.signozBootData.settings.sentry.dsn,
|
||||
tunnel: window.signozBootData.settings.sentry.tunnel,
|
||||
environment: process.env.ENVIRONMENT,
|
||||
release: process.env.VERSION,
|
||||
integrations: [
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
import { legacyCreateProxyMiddleware } from 'http-proxy-middleware';
|
||||
|
||||
export default function (app) {
|
||||
app.use(
|
||||
'/tunnel',
|
||||
legacyCreateProxyMiddleware({
|
||||
target: `${process.env.TUNNEL_DOMAIN}/tunnel`,
|
||||
changeOrigin: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
10
frontend/src/vite-env.d.ts
vendored
10
frontend/src/vite-env.d.ts
vendored
@@ -11,18 +11,24 @@ declare module '*.md?raw' {
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_BASE_PATH: string;
|
||||
readonly VITE_FRONTEND_API_ENDPOINT: string;
|
||||
readonly VITE_WEBSOCKET_API_ENDPOINT: string;
|
||||
readonly VITE_PYLON_ENABLED: string;
|
||||
readonly VITE_PYLON_APP_ID: string;
|
||||
readonly VITE_PYLON_IDENTITY_SECRET: string;
|
||||
readonly VITE_APPCUES_ENABLED: string;
|
||||
readonly VITE_APPCUES_APP_ID: string;
|
||||
readonly VITE_POSTHOG_ENABLED: string;
|
||||
readonly VITE_POSTHOG_API_HOST: string;
|
||||
readonly VITE_POSTHOG_KEY: string;
|
||||
readonly VITE_POSTHOG_UI_HOST: string;
|
||||
readonly VITE_SENTRY_AUTH_TOKEN: string;
|
||||
readonly VITE_SENTRY_ORG: string;
|
||||
readonly VITE_SENTRY_PROJECT_ID: string;
|
||||
readonly VITE_SENTRY_ENABLED: string;
|
||||
readonly VITE_SENTRY_TUNNEL: string;
|
||||
readonly VITE_SENTRY_DSN: string;
|
||||
readonly VITE_TUNNEL_URL: string;
|
||||
readonly VITE_TUNNEL_DOMAIN: string;
|
||||
readonly VITE_DOCS_BASE_URL: string;
|
||||
readonly VITE_ENVIRONMENT: string;
|
||||
readonly VITE_VERSION: string;
|
||||
|
||||
@@ -6,9 +6,15 @@ import type { Plugin, TransformResult, UserConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import vitePluginChecker from 'vite-plugin-checker';
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import type {
|
||||
Appcues,
|
||||
Posthog,
|
||||
Pylon,
|
||||
Sentry,
|
||||
WebSettings,
|
||||
} from 'types/generated/webSettings';
|
||||
|
||||
// In dev the Go backend is not involved, so replace the [[.BaseHref]] placeholder
|
||||
// with the configured base path so relative assets resolve correctly from the Vite dev server.
|
||||
@@ -23,17 +29,33 @@ function devBasePathPlugin(basePath: string): Plugin {
|
||||
};
|
||||
}
|
||||
|
||||
function devBootDataPlugin(env: Record<string, string>): Plugin {
|
||||
function devBootDataPlugin(env: ImportMetaEnv): Plugin {
|
||||
return {
|
||||
name: 'dev-boot-data',
|
||||
apply: 'serve',
|
||||
transformIndexHtml(html): string {
|
||||
const settings = {
|
||||
posthog: { enabled: env.VITE_POSTHOG_ENABLED !== 'false' },
|
||||
appcues: { enabled: env.VITE_APPCUES_ENABLED !== 'false' },
|
||||
sentry: { enabled: env.VITE_SENTRY_ENABLED !== 'false' },
|
||||
pylon: { enabled: env.VITE_PYLON_ENABLED !== 'false' },
|
||||
};
|
||||
posthog: {
|
||||
enabled: env.VITE_POSTHOG_ENABLED === 'true',
|
||||
apiHost: env.VITE_POSTHOG_API_HOST || '',
|
||||
key: env.VITE_POSTHOG_KEY || '',
|
||||
uiHost: env.VITE_POSTHOG_UI_HOST || '',
|
||||
} satisfies Required<Posthog>,
|
||||
appcues: {
|
||||
enabled: env.VITE_APPCUES_ENABLED === 'true',
|
||||
appId: env.VITE_APPCUES_APP_ID || '',
|
||||
} satisfies Required<Appcues>,
|
||||
sentry: {
|
||||
enabled: env.VITE_SENTRY_ENABLED === 'true',
|
||||
dsn: env.VITE_SENTRY_DSN || '',
|
||||
tunnel: env.VITE_SENTRY_TUNNEL || '',
|
||||
} satisfies Required<Sentry>,
|
||||
pylon: {
|
||||
enabled: env.VITE_PYLON_ENABLED === 'true',
|
||||
appId: env.VITE_PYLON_APP_ID || '',
|
||||
identitySecret: env.VITE_PYLON_IDENTITY_SECRET || '',
|
||||
} satisfies Required<Pylon>,
|
||||
} satisfies Required<WebSettings>;
|
||||
return html.replaceAll('[[.Settings]]', JSON.stringify(settings));
|
||||
},
|
||||
};
|
||||
@@ -55,7 +77,7 @@ function rawMarkdownPlugin(): Plugin {
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }): UserConfig => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
const env = loadEnv(mode, process.cwd(), '') as ImportMetaEnv;
|
||||
// Base path for serving the app (e.g., '/signoz/'). Defaults to '/'.
|
||||
const basePath = env.VITE_BASE_PATH || '/';
|
||||
|
||||
@@ -65,14 +87,6 @@ export default defineConfig(({ mode }): UserConfig => {
|
||||
devBasePathPlugin(basePath),
|
||||
devBootDataPlugin(env),
|
||||
react(),
|
||||
createHtmlPlugin({
|
||||
inject: {
|
||||
data: {
|
||||
PYLON_APP_ID: env.VITE_PYLON_APP_ID || '',
|
||||
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID || '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
vitePluginChecker({
|
||||
typescript: true,
|
||||
// this doubles the build tim
|
||||
@@ -157,17 +171,6 @@ export default defineConfig(({ mode }): UserConfig => {
|
||||
'process.env.WEBSOCKET_API_ENDPOINT': JSON.stringify(
|
||||
env.VITE_WEBSOCKET_API_ENDPOINT,
|
||||
),
|
||||
'process.env.PYLON_APP_ID': JSON.stringify(env.VITE_PYLON_APP_ID),
|
||||
'process.env.PYLON_IDENTITY_SECRET': JSON.stringify(
|
||||
env.VITE_PYLON_IDENTITY_SECRET,
|
||||
),
|
||||
'process.env.APPCUES_APP_ID': JSON.stringify(env.VITE_APPCUES_APP_ID),
|
||||
'process.env.POSTHOG_KEY': JSON.stringify(env.VITE_POSTHOG_KEY),
|
||||
'process.env.SENTRY_ORG': JSON.stringify(env.VITE_SENTRY_ORG),
|
||||
'process.env.SENTRY_PROJECT_ID': JSON.stringify(env.VITE_SENTRY_PROJECT_ID),
|
||||
'process.env.SENTRY_DSN': JSON.stringify(env.VITE_SENTRY_DSN),
|
||||
'process.env.TUNNEL_URL': JSON.stringify(env.VITE_TUNNEL_URL),
|
||||
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
|
||||
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
|
||||
'process.env.ENVIRONMENT': JSON.stringify(env.VITE_ENVIRONMENT),
|
||||
'process.env.VERSION': JSON.stringify(env.VITE_VERSION),
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
)
|
||||
|
||||
type migrateCommon struct {
|
||||
@@ -24,10 +23,119 @@ func NewMigrateCommon(logger *slog.Logger) *migrateCommon {
|
||||
}
|
||||
}
|
||||
|
||||
// WrapInV5Envelope delegates to querybuildertypesv5.WrapInV5Envelope; the
|
||||
// transform is stateless and shared with the v1→v2 dashboard conversion.
|
||||
func (migration *migrateCommon) WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any {
|
||||
return querybuildertypesv5.WrapInV5Envelope(name, queryMap, queryType)
|
||||
// Create a properly structured v5 query
|
||||
v5Query := map[string]any{
|
||||
"name": name,
|
||||
"disabled": queryMap["disabled"],
|
||||
"legend": queryMap["legend"],
|
||||
}
|
||||
|
||||
if name != queryMap["expression"] {
|
||||
// formula
|
||||
queryType = "builder_formula"
|
||||
v5Query["expression"] = queryMap["expression"]
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
}
|
||||
|
||||
// Add signal based on data source
|
||||
if dataSource, ok := queryMap["dataSource"].(string); ok {
|
||||
switch dataSource {
|
||||
case "traces":
|
||||
v5Query["signal"] = "traces"
|
||||
case "logs":
|
||||
v5Query["signal"] = "logs"
|
||||
case "metrics":
|
||||
v5Query["signal"] = "metrics"
|
||||
}
|
||||
}
|
||||
|
||||
if stepInterval, ok := queryMap["stepInterval"]; ok {
|
||||
v5Query["stepInterval"] = stepInterval
|
||||
}
|
||||
|
||||
if aggregations, ok := queryMap["aggregations"]; ok {
|
||||
v5Query["aggregations"] = aggregations
|
||||
}
|
||||
|
||||
if filter, ok := queryMap["filter"]; ok {
|
||||
v5Query["filter"] = filter
|
||||
}
|
||||
|
||||
// Copy groupBy with proper structure
|
||||
if groupBy, ok := queryMap["groupBy"].([]any); ok {
|
||||
v5GroupBy := make([]any, len(groupBy))
|
||||
for i, gb := range groupBy {
|
||||
if gbMap, ok := gb.(map[string]any); ok {
|
||||
v5GroupBy[i] = map[string]any{
|
||||
"name": gbMap["key"],
|
||||
"fieldDataType": gbMap["dataType"],
|
||||
"fieldContext": gbMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["groupBy"] = v5GroupBy
|
||||
}
|
||||
|
||||
// Copy orderBy with proper structure
|
||||
if orderBy, ok := queryMap["orderBy"].([]any); ok {
|
||||
v5OrderBy := make([]any, len(orderBy))
|
||||
for i, ob := range orderBy {
|
||||
if obMap, ok := ob.(map[string]any); ok {
|
||||
v5OrderBy[i] = map[string]any{
|
||||
"key": map[string]any{
|
||||
"name": obMap["columnName"],
|
||||
"fieldDataType": obMap["dataType"],
|
||||
"fieldContext": obMap["type"],
|
||||
},
|
||||
"direction": obMap["order"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["order"] = v5OrderBy
|
||||
}
|
||||
|
||||
// Copy selectColumns as selectFields
|
||||
if selectColumns, ok := queryMap["selectColumns"].([]any); ok {
|
||||
v5SelectFields := make([]any, len(selectColumns))
|
||||
for i, col := range selectColumns {
|
||||
if colMap, ok := col.(map[string]any); ok {
|
||||
v5SelectFields[i] = map[string]any{
|
||||
"name": colMap["key"],
|
||||
"fieldDataType": colMap["dataType"],
|
||||
"fieldContext": colMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["selectFields"] = v5SelectFields
|
||||
}
|
||||
|
||||
// Copy limit and offset
|
||||
if limit, ok := queryMap["limit"]; ok {
|
||||
v5Query["limit"] = limit
|
||||
}
|
||||
if offset, ok := queryMap["offset"]; ok {
|
||||
v5Query["offset"] = offset
|
||||
}
|
||||
|
||||
if having, ok := queryMap["having"]; ok {
|
||||
v5Query["having"] = having
|
||||
}
|
||||
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *migrateCommon) updateQueryData(ctx context.Context, queryData map[string]any, version, widgetType string) bool {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/transition"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -405,34 +406,27 @@ func (dashboard *Dashboard) GetWidgetQuery(startTime, endTime, widgetIndex uint6
|
||||
widgetData := data.Widgets[widgetIndex]
|
||||
switch widgetData.Query.QueryType {
|
||||
case "builder":
|
||||
isRawRequest := dashboard.getQueryRequestTypeFromPanelType(widgetData.PanelTypes) == querybuildertypesv5.RequestTypeRaw
|
||||
migrate := transition.NewMigrateCommon(logger)
|
||||
for _, query := range widgetData.Query.Builder.QueryData {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
// build aggregations the same way the frontend does before hitting the query
|
||||
// range API; raw requests carry no aggregations.
|
||||
if isRawRequest {
|
||||
delete(query, "aggregations")
|
||||
} else {
|
||||
query["aggregations"] = querybuildertypesv5.CreateAggregation(query, widgetData.PanelTypes)
|
||||
}
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_query"))
|
||||
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_query"))
|
||||
}
|
||||
for _, query := range widgetData.Query.Builder.QueryFormulas {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_formula"))
|
||||
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_formula"))
|
||||
}
|
||||
for _, query := range widgetData.Query.Builder.QueryTraceOperator {
|
||||
queryName, ok := query["queryName"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
|
||||
}
|
||||
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_trace_operator"))
|
||||
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_trace_operator"))
|
||||
}
|
||||
case "clickhouse_sql":
|
||||
for _, query := range widgetData.Query.ClickhouseSQL {
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WrapInV5Envelope translates a single v4 builder query/formula map into a
|
||||
// v5 query envelope ({"type": ..., "spec": ...}). It is a pure shape transform
|
||||
// over untyped maps: v4 builder field names (groupBy/orderBy/selectColumns/
|
||||
// dataSource) are rewritten to their v5 equivalents and a `signal` is derived
|
||||
// from the data source. queryType selects the envelope type, except a formula
|
||||
// (detected when name != queryMap["expression"]) is always emitted as
|
||||
// "builder_formula".
|
||||
//
|
||||
// Migration code (pkg/transition) and the v1→v2 dashboard conversion both
|
||||
// produce v5 envelopes, so this lives here with the v5 query types rather than
|
||||
// in an infra-level package.
|
||||
func WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any {
|
||||
// Create a properly structured v5 query
|
||||
v5Query := map[string]any{
|
||||
"name": name,
|
||||
"disabled": queryMap["disabled"],
|
||||
"legend": queryMap["legend"],
|
||||
}
|
||||
|
||||
if name != queryMap["expression"] {
|
||||
// formula
|
||||
queryType = "builder_formula"
|
||||
v5Query["expression"] = queryMap["expression"]
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
}
|
||||
|
||||
// Add signal based on data source
|
||||
if dataSource, ok := queryMap["dataSource"].(string); ok {
|
||||
switch dataSource {
|
||||
case "traces":
|
||||
v5Query["signal"] = "traces"
|
||||
case "logs":
|
||||
v5Query["signal"] = "logs"
|
||||
case "metrics":
|
||||
v5Query["signal"] = "metrics"
|
||||
}
|
||||
}
|
||||
|
||||
if stepInterval, ok := queryMap["stepInterval"]; ok {
|
||||
v5Query["stepInterval"] = stepInterval
|
||||
}
|
||||
|
||||
if aggregations, ok := queryMap["aggregations"]; ok {
|
||||
v5Query["aggregations"] = aggregations
|
||||
}
|
||||
|
||||
if filter, ok := queryMap["filter"]; ok {
|
||||
v5Query["filter"] = filter
|
||||
}
|
||||
|
||||
// Copy groupBy with proper structure
|
||||
if groupBy, ok := queryMap["groupBy"].([]any); ok {
|
||||
v5GroupBy := make([]any, len(groupBy))
|
||||
for i, gb := range groupBy {
|
||||
if gbMap, ok := gb.(map[string]any); ok {
|
||||
v5GroupBy[i] = map[string]any{
|
||||
"name": gbMap["key"],
|
||||
"fieldDataType": gbMap["dataType"],
|
||||
"fieldContext": gbMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["groupBy"] = v5GroupBy
|
||||
}
|
||||
|
||||
// Copy orderBy with proper structure
|
||||
if orderBy, ok := queryMap["orderBy"].([]any); ok {
|
||||
v5OrderBy := make([]any, len(orderBy))
|
||||
for i, ob := range orderBy {
|
||||
if obMap, ok := ob.(map[string]any); ok {
|
||||
v5OrderBy[i] = map[string]any{
|
||||
"key": map[string]any{
|
||||
"name": obMap["columnName"],
|
||||
"fieldDataType": obMap["dataType"],
|
||||
"fieldContext": obMap["type"],
|
||||
},
|
||||
"direction": obMap["order"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["order"] = v5OrderBy
|
||||
}
|
||||
|
||||
// Copy selectColumns as selectFields
|
||||
if selectColumns, ok := queryMap["selectColumns"].([]any); ok {
|
||||
v5SelectFields := make([]any, len(selectColumns))
|
||||
for i, col := range selectColumns {
|
||||
if colMap, ok := col.(map[string]any); ok {
|
||||
v5SelectFields[i] = map[string]any{
|
||||
"name": colMap["key"],
|
||||
"fieldDataType": colMap["dataType"],
|
||||
"fieldContext": colMap["type"],
|
||||
}
|
||||
}
|
||||
}
|
||||
v5Query["selectFields"] = v5SelectFields
|
||||
}
|
||||
|
||||
// Copy limit and offset
|
||||
if limit, ok := queryMap["limit"]; ok {
|
||||
v5Query["limit"] = limit
|
||||
}
|
||||
if offset, ok := queryMap["offset"]; ok {
|
||||
v5Query["offset"] = offset
|
||||
}
|
||||
|
||||
if having, ok := queryMap["having"]; ok {
|
||||
v5Query["having"] = having
|
||||
}
|
||||
|
||||
if functions, ok := queryMap["functions"]; ok {
|
||||
v5Query["functions"] = functions
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"type": queryType,
|
||||
"spec": v5Query,
|
||||
}
|
||||
}
|
||||
|
||||
// aggregationExprRegexp matches a function-style aggregation like `count()` or
|
||||
// `sum(field)` with an optional `as <alias>`, as the frontend's parseAggregations does.
|
||||
var aggregationExprRegexp = regexp.MustCompile(`([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+((?:'[^']*'|"[^"]*"|[a-zA-Z0-9_-]+)))?`)
|
||||
|
||||
// CreateAggregation builds the v5 aggregations for a stored builder query, mirroring
|
||||
// createAggregation in the frontend's prepareQueryRangePayloadV5.ts. Metrics yield a
|
||||
// single structured aggregation; logs/traces split their comma-separated expression into
|
||||
// one aggregation per call, defaulting to count() when nothing parses.
|
||||
func CreateAggregation(queryData map[string]any, panelType string) []any {
|
||||
if queryData == nil {
|
||||
return []any{}
|
||||
}
|
||||
|
||||
if dataSource, _ := queryData["dataSource"].(string); dataSource == "metrics" {
|
||||
var first map[string]any
|
||||
if aggs, ok := queryData["aggregations"].([]any); ok && len(aggs) > 0 {
|
||||
first, _ = aggs[0].(map[string]any)
|
||||
}
|
||||
attribute, _ := queryData["aggregateAttribute"].(map[string]any)
|
||||
|
||||
metric := map[string]any{}
|
||||
setFirstNonEmpty(metric, "metricName", first["metricName"], attribute["key"])
|
||||
setFirstNonEmpty(metric, "temporality", first["temporality"], attribute["temporality"])
|
||||
setFirstNonEmpty(metric, "timeAggregation", first["timeAggregation"], queryData["timeAggregation"])
|
||||
setFirstNonEmpty(metric, "spaceAggregation", first["spaceAggregation"], queryData["spaceAggregation"])
|
||||
if panelType == "table" || panelType == "pie" || panelType == "value" {
|
||||
setFirstNonEmpty(metric, "reduceTo", first["reduceTo"], queryData["reduceTo"])
|
||||
}
|
||||
return []any{metric}
|
||||
}
|
||||
|
||||
aggs, ok := queryData["aggregations"].([]any)
|
||||
if !ok || len(aggs) == 0 {
|
||||
return []any{map[string]any{"expression": "count()"}}
|
||||
}
|
||||
|
||||
result := []any{}
|
||||
for _, agg := range aggs {
|
||||
aggMap, _ := agg.(map[string]any)
|
||||
expression, _ := aggMap["expression"].(string)
|
||||
alias, _ := aggMap["alias"].(string)
|
||||
parsed := parseAggregations(expression, alias)
|
||||
if len(parsed) == 0 {
|
||||
result = append(result, map[string]any{"expression": "count()"})
|
||||
continue
|
||||
}
|
||||
result = append(result, parsed...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parseAggregations extracts each function-style call from a (possibly comma-separated)
|
||||
// aggregation expression, attaching the inline `as` alias or the fallback alias.
|
||||
func parseAggregations(expression, fallbackAlias string) []any {
|
||||
result := []any{}
|
||||
for _, match := range aggregationExprRegexp.FindAllStringSubmatch(expression, -1) {
|
||||
agg := map[string]any{"expression": match[1]}
|
||||
if alias := match[2]; alias != "" {
|
||||
agg["alias"] = strings.Trim(alias, `'"`)
|
||||
} else if fallbackAlias != "" {
|
||||
agg["alias"] = fallbackAlias
|
||||
}
|
||||
result = append(result, agg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// setFirstNonEmpty sets key to the first value that is neither nil nor "", mirroring the
|
||||
// JS `a || b` fallback the frontend uses for the metric aggregation fields.
|
||||
func setFirstNonEmpty(target map[string]any, key string, values ...any) {
|
||||
for _, v := range values {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if s, ok := v.(string); ok && s == "" {
|
||||
continue
|
||||
}
|
||||
target[key] = v
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateAggregation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
queryData map[string]any
|
||||
panelType string
|
||||
expectedOutput []any
|
||||
}{
|
||||
{
|
||||
description: "nil query data yields no aggregations",
|
||||
queryData: nil,
|
||||
expectedOutput: []any{},
|
||||
},
|
||||
{
|
||||
description: "single logs expression is left untouched",
|
||||
queryData: map[string]any{"dataSource": "logs", "aggregations": []any{map[string]any{"expression": "count()"}}},
|
||||
expectedOutput: []any{map[string]any{"expression": "count()"}},
|
||||
},
|
||||
{
|
||||
description: "comma separated trace expressions are split into one object each",
|
||||
queryData: map[string]any{"dataSource": "traces", "aggregations": []any{map[string]any{"expression": "count(), sum(price)"}}},
|
||||
expectedOutput: []any{
|
||||
map[string]any{"expression": "count()"},
|
||||
map[string]any{"expression": "sum(price)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "inline alias is preserved and unquoted",
|
||||
queryData: map[string]any{"dataSource": "logs", "aggregations": []any{map[string]any{"expression": "count() as 'total', sum(price) as revenue"}}},
|
||||
expectedOutput: []any{
|
||||
map[string]any{"expression": "count()", "alias": "total"},
|
||||
map[string]any{"expression": "sum(price)", "alias": "revenue"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "space separated expressions split with an unquoted alias on the first only",
|
||||
queryData: map[string]any{"dataSource": "logs", "aggregations": []any{map[string]any{"expression": "count() as cnt avg(code.lineno) "}}},
|
||||
expectedOutput: []any{
|
||||
map[string]any{"expression": "count()", "alias": "cnt"},
|
||||
map[string]any{"expression": "avg(code.lineno)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "fallback alias is applied when expression has no inline alias",
|
||||
queryData: map[string]any{"dataSource": "logs", "aggregations": []any{map[string]any{"expression": "count()", "alias": "hits"}}},
|
||||
expectedOutput: []any{map[string]any{"expression": "count()", "alias": "hits"}},
|
||||
},
|
||||
{
|
||||
description: "commas inside function arguments do not split the expression",
|
||||
queryData: map[string]any{"dataSource": "traces", "aggregations": []any{map[string]any{"expression": "countIf(day > 10, status)"}}},
|
||||
expectedOutput: []any{map[string]any{"expression": "countIf(day > 10, status)"}},
|
||||
},
|
||||
{
|
||||
description: "unparseable expression falls back to count()",
|
||||
queryData: map[string]any{"dataSource": "logs", "aggregations": []any{map[string]any{"expression": "not-an-aggregation"}}},
|
||||
expectedOutput: []any{map[string]any{"expression": "count()"}},
|
||||
},
|
||||
{
|
||||
description: "empty aggregations fall back to count()",
|
||||
queryData: map[string]any{"dataSource": "logs", "aggregations": []any{}},
|
||||
expectedOutput: []any{map[string]any{"expression": "count()"}},
|
||||
},
|
||||
{
|
||||
description: "missing aggregations fall back to count()",
|
||||
queryData: map[string]any{"dataSource": "traces"},
|
||||
expectedOutput: []any{map[string]any{"expression": "count()"}},
|
||||
},
|
||||
{
|
||||
description: "metric aggregation is built from the first aggregation",
|
||||
queryData: map[string]any{
|
||||
"dataSource": "metrics",
|
||||
"aggregations": []any{map[string]any{
|
||||
"metricName": "http_requests_total",
|
||||
"temporality": "delta",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
}},
|
||||
},
|
||||
expectedOutput: []any{map[string]any{
|
||||
"metricName": "http_requests_total",
|
||||
"temporality": "delta",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
}},
|
||||
},
|
||||
{
|
||||
description: "metric omits temporality when empty, matching the frontend `|| undefined`",
|
||||
panelType: "table",
|
||||
queryData: map[string]any{
|
||||
"dataSource": "metrics",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "avg",
|
||||
"temporality": "",
|
||||
"reduceTo": "avg",
|
||||
"aggregations": []any{map[string]any{
|
||||
"metricName": "cpu_usage",
|
||||
"temporality": "",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "avg",
|
||||
"reduceTo": "avg",
|
||||
}},
|
||||
},
|
||||
expectedOutput: []any{map[string]any{
|
||||
"metricName": "cpu_usage",
|
||||
"timeAggregation": "sum",
|
||||
"spaceAggregation": "avg",
|
||||
"reduceTo": "avg",
|
||||
}},
|
||||
},
|
||||
{
|
||||
description: "metric includes reduceTo for table/pie/value panels",
|
||||
panelType: "table",
|
||||
queryData: map[string]any{
|
||||
"dataSource": "metrics",
|
||||
"aggregations": []any{map[string]any{
|
||||
"metricName": "http_requests_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg",
|
||||
}},
|
||||
},
|
||||
expectedOutput: []any{map[string]any{
|
||||
"metricName": "http_requests_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg",
|
||||
}},
|
||||
},
|
||||
{
|
||||
description: "metric drops reduceTo for other panels even when query data has it",
|
||||
panelType: "graph",
|
||||
queryData: map[string]any{
|
||||
"dataSource": "metrics",
|
||||
"aggregations": []any{map[string]any{
|
||||
"metricName": "http_requests_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "avg",
|
||||
}},
|
||||
},
|
||||
expectedOutput: []any{map[string]any{
|
||||
"metricName": "http_requests_total",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
}},
|
||||
},
|
||||
{
|
||||
description: "metric falls back to legacy aggregateAttribute and top-level fields",
|
||||
queryData: map[string]any{
|
||||
"dataSource": "metrics",
|
||||
"aggregateAttribute": map[string]any{"key": "legacy_metric", "temporality": "cumulative"},
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max",
|
||||
},
|
||||
expectedOutput: []any{map[string]any{
|
||||
"metricName": "legacy_metric",
|
||||
"temporality": "cumulative",
|
||||
"timeAggregation": "avg",
|
||||
"spaceAggregation": "max",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expectedOutput, CreateAggregation(testCase.queryData, testCase.panelType))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
@@ -9,7 +8,6 @@ from sqlalchemy import sql
|
||||
from wiremock.resources.mappings import Mapping
|
||||
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker
|
||||
|
||||
@@ -207,147 +205,6 @@ def test_public_dashboard_widget_query_range(
|
||||
assert resp.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
|
||||
def test_public_dashboard_widget_query_range_multi_aggregation(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[TestContainerDocker, list[Mapping]], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
):
|
||||
"""
|
||||
A logs/traces widget stores several aggregations as one comma-separated expression
|
||||
(e.g. "count(), sum(latency_ms)"). The public widget query path must split it into
|
||||
one aggregation per call, mirroring the frontend, before handing it to the querier.
|
||||
If the split does not happen the querier receives a single malformed aggregation and
|
||||
the request fails - so a successful response with two aggregations proves the split.
|
||||
"""
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Unique per-run service so the widget query only sees this run's logs.
|
||||
service_name = f"multiagg-public-{uuid.uuid4()}"
|
||||
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
insert_logs(
|
||||
[
|
||||
Logs(
|
||||
timestamp=now - timedelta(minutes=5),
|
||||
resources={"service.name": service_name},
|
||||
attributes={"latency_ms": 100},
|
||||
body="multi-agg log 1",
|
||||
),
|
||||
Logs(
|
||||
timestamp=now - timedelta(minutes=3),
|
||||
resources={"service.name": service_name},
|
||||
attributes={"latency_ms": 200},
|
||||
body="multi-agg log 2",
|
||||
),
|
||||
Logs(
|
||||
timestamp=now - timedelta(minutes=1),
|
||||
resources={"service.name": service_name},
|
||||
attributes={"latency_ms": 300},
|
||||
body="multi-agg log 3",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
dashboard_req = {
|
||||
"title": "Multi Aggregation Public Widget",
|
||||
"description": "Comma-separated aggregations must be split on the public query path",
|
||||
"version": "v5",
|
||||
"widgets": [
|
||||
{
|
||||
"id": "b2c0a1d4-9f3e-4c2a-8a7b-1e2f3a4b5c6d",
|
||||
"panelTypes": "graph",
|
||||
"query": {
|
||||
"builder": {
|
||||
"queryData": [
|
||||
{
|
||||
"aggregations": [{"expression": "count(), sum(latency_ms)"}],
|
||||
"dataSource": "logs",
|
||||
"disabled": False,
|
||||
"expression": "A",
|
||||
"filter": {"expression": f"service.name = '{service_name}'"},
|
||||
"functions": [],
|
||||
"groupBy": [],
|
||||
"having": {"expression": ""},
|
||||
"legend": "",
|
||||
"limit": 10,
|
||||
"orderBy": [],
|
||||
"queryName": "A",
|
||||
"source": "",
|
||||
"stepInterval": 60,
|
||||
}
|
||||
],
|
||||
"queryFormulas": [],
|
||||
"queryTraceOperator": [],
|
||||
},
|
||||
"clickhouse_sql": [{"disabled": False, "legend": "", "name": "A", "query": ""}],
|
||||
"id": "c3d1b2e5-0a4f-4d3b-9b8c-2f3a4b5c6d7e",
|
||||
"promql": [{"disabled": False, "legend": "", "name": "A", "query": ""}],
|
||||
"queryType": "builder",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
create_response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/dashboards"),
|
||||
json=dashboard_req,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert create_response.status_code == HTTPStatus.CREATED
|
||||
dashboard_id = create_response.json()["data"]["id"]
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/dashboards/{dashboard_id}/public"),
|
||||
json={"timeRangeEnabled": False, "defaultTimeRange": "30m"},
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/dashboards/{dashboard_id}/public"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
public_path = response.json()["data"]["publicPath"]
|
||||
public_dashboard_id = public_path.split("/public/dashboard/")[-1]
|
||||
|
||||
resp = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/public/dashboards/{public_dashboard_id}/widgets/0/query_range"),
|
||||
timeout=5,
|
||||
)
|
||||
assert resp.status_code == HTTPStatus.OK
|
||||
body = resp.json()
|
||||
assert body["status"] == "success"
|
||||
|
||||
# The single "count(), sum(latency_ms)" expression must have been split into two
|
||||
# separate aggregations on the way to the querier.
|
||||
results = body["data"]["data"]["results"]
|
||||
assert len(results) == 1
|
||||
|
||||
aggregations = results[0]["aggregations"]
|
||||
assert len(aggregations) == 2
|
||||
|
||||
# With no group-by each aggregation produces a single series.
|
||||
for aggregation in aggregations:
|
||||
assert len(aggregation["series"]) == 1
|
||||
assert len(aggregation["series"][0]["values"]) > 0
|
||||
|
||||
# Each aggregation is computed independently over the three logs: count() totals 3,
|
||||
# sum(latency_ms) totals 100 + 200 + 300 = 600. Summing each aggregation's points is
|
||||
# robust to step bucketing and to the order the aggregations come back in.
|
||||
aggregation_totals = sorted(
|
||||
sum(point["value"] for series in aggregation["series"] for point in series["values"])
|
||||
for aggregation in aggregations
|
||||
)
|
||||
assert aggregation_totals == [3, 600]
|
||||
|
||||
|
||||
def test_anonymous_role_has_public_dashboard_permission(
|
||||
request: pytest.FixtureRequest,
|
||||
signoz: SigNoz,
|
||||
|
||||
Reference in New Issue
Block a user