This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/2.0.0 by this push:
     new ceedadc3bd Add service overview live proof
ceedadc3bd is described below

commit ceedadc3bd9bf9ee75e96e2d53778dac217d796d
Author: Logic <[email protected]>
AuthorDate: Mon Jun 8 09:39:08 2026 +0800

    Add service overview live proof
---
 script/dev/run-three-signal-live-proof.sh          |   2 +-
 ...ashboard-source-edit-live-browser-smoke.spec.ts | 133 +++++++++++++++++++++
 ...ashboard-source-edit-live-browser-smoke.test.ts |  21 ++++
 .../three-signal-live-proof-contract.test.ts       |   2 +-
 4 files changed, 156 insertions(+), 2 deletions(-)

diff --git a/script/dev/run-three-signal-live-proof.sh 
b/script/dev/run-three-signal-live-proof.sh
index 9fb35c3310..5976d884e4 100755
--- a/script/dev/run-three-signal-live-proof.sh
+++ b/script/dev/run-three-signal-live-proof.sh
@@ -15,7 +15,7 @@ HERTZBEAT_PASSWORD="${HERTZBEAT_PASSWORD:-hertzbeat}"
 
SPRING_DATASOURCE_URL="${SPRING_DATASOURCE_URL:-jdbc:h2:mem:hb_live_smoke;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE}"
 BACKEND_LOG="${BACKEND_LOG:-/tmp/hb-three-signal-live-backend.log}"
 FRONTEND_LOG="${FRONTEND_LOG:-/tmp/hb-three-signal-live-frontend.log}"
-PLAYWRIGHT_GREP="${PLAYWRIGHT_GREP:-promotes logs, traces, and metrics saved 
views into a persisted replay dashboard}"
+PLAYWRIGHT_GREP="${PLAYWRIGHT_GREP:-promotes logs, traces, and metrics saved 
views into a persisted replay dashboard|saves a service overview dashboard from 
URL service and entity context}"
 BACKEND_READY_PATH="${BACKEND_READY_PATH:-/actuator/health}"
 READY_ATTEMPTS="${READY_ATTEMPTS:-90}"
 READY_SLEEP_SECONDS="${READY_SLEEP_SECONDS:-2}"
diff --git a/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts 
b/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts
index f922c18fb0..a214d54b57 100644
--- a/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts
+++ b/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts
@@ -3,6 +3,7 @@ import { expect, test, type APIRequestContext, type Page } from 
'playwright/test
 import {
   buildSignalDashboardCompositionFromDrafts,
   buildSignalDashboardExecutionPlans,
+  buildSignalServiceOverviewDashboard,
   createSignalDashboardPanelDraftFromRuntimeBreakout,
   createSignalDashboardPanelDraftFromRuntimeEvidence,
   type SignalDashboardRuntimeSyncTooltipRow
@@ -156,6 +157,23 @@ function uniqueSavedViewReplayDashboardKey() {
   return 
`${DASHBOARD_KEY_PREFIX}-saved-view-replay-${randomBytes(4).toString('hex')}`;
 }
 
+function serviceOverviewRoute() {
+  return [
+    '/dashboard?serviceName=checkout',
+    'serviceNamespace=payments',
+    'environment=prod',
+    'entityId=4200',
+    'entityType=service',
+    'entityName=Checkout%20API',
+    'source=otlp',
+    'collector=collector-a',
+    'template=spring-boot',
+    'timeRange=last-1h',
+    'refresh=30',
+    'live=true'
+  ].join('&');
+}
+
 function titleFromRoute(signalCase: SignalCase, route: string) {
   if (signalCase.signal === 'logs') return route.includes('latency') ? 
'latency' : 'timeout';
   if (signalCase.signal === 'traces') return route.includes('GET+%2Fbilling') 
? 'GET /billing' : 'POST /checkout';
@@ -467,6 +485,121 @@ function assertPlanMatchesReplayExpectation(
 }
 
 test.describe('live dashboard source edit browser smoke', () => {
+  test('saves a service overview dashboard from URL service and entity 
context', async ({ page }) => {
+    test.setTimeout(BROWSER_SMOKE_TIMEOUT);
+
+    const expectedDashboard = buildSignalServiceOverviewDashboard({
+      serviceName: 'checkout',
+      serviceNamespace: 'payments',
+      environment: 'prod',
+      entityId: '4200',
+      entityType: 'service',
+      entityName: 'Checkout API',
+      source: 'otlp',
+      collector: 'collector-a',
+      template: 'spring-boot',
+      timeRange: 'last-1h',
+      refresh: '30',
+      live: 'true'
+    });
+    const dashboardKey = expectedDashboard.dashboardKey;
+    await authenticate(page);
+    await 
page.context().request.delete(`${baseUrl}/api/signal/dashboard/${encodeURIComponent(dashboardKey)}`,
 {
+      failOnStatusCode: false
+    });
+
+    try {
+      await page.goto(`${baseUrl}${serviceOverviewRoute()}`, {
+        timeout: BROWSER_SMOKE_TIMEOUT,
+        waitUntil: 'domcontentloaded'
+      });
+
+      const serviceOverviewAction = 
page.locator('[data-dashboard-service-overview-action="save"]').first();
+      await 
expect(page.locator('[data-dashboard-service-overview-context="ready"]')).toHaveAttribute(
+        'data-dashboard-service-overview-service',
+        'checkout',
+        { timeout: WORKBENCH_READY_TIMEOUT }
+      );
+      await 
expect(serviceOverviewAction).toHaveAttribute('data-dashboard-service-overview-action-state',
 'ready');
+      await 
expect(serviceOverviewAction).toHaveAttribute('data-dashboard-service-overview-action-service',
 'checkout');
+      await serviceOverviewAction.click();
+
+      await expect(page).toHaveURL(/dashboard=service-checkout-overview/, {
+        timeout: WORKBENCH_READY_TIMEOUT
+      });
+      await 
expect(page.locator('[data-dashboard-composition-preview-panel]')).toHaveCount(18,
 {
+        timeout: WORKBENCH_READY_TIMEOUT
+      });
+
+      await expect.poll(async () => {
+        const persisted = await loadDashboard(page.context().request, 
dashboardKey);
+        return parseWidgets(persisted).length;
+      }, {
+        timeout: WORKBENCH_READY_TIMEOUT
+      }).toBe(18);
+
+      const persistedDashboard = await loadDashboard(page.context().request, 
dashboardKey);
+      const persistedWidgets = parseWidgets(persistedDashboard);
+      const persistedVariables = parseVariables(persistedDashboard);
+      expect(persistedDashboard).toEqual(expect.objectContaining({
+        dashboardKey: 'service-checkout-overview',
+        title: 'Checkout API service overview',
+        tags: 'service,apm,metrics,logs,traces,alerts'
+      }));
+      expect(persistedWidgets.map(widget => 
widget.title)).toEqual(expect.arrayContaining([
+        'Service overview request rate: service.name=checkout',
+        'Service overview error rate: service.name=checkout',
+        'Service overview apdex: service.name=checkout',
+        'Service overview log errors: service.name=checkout',
+        'Service overview exceptions: service.name=checkout',
+        'Service overview firing alerts: service.name=checkout'
+      ]));
+      expect(persistedVariables).toEqual(expect.arrayContaining([
+        expect.objectContaining({ name: 'service.name', type: 'query', value: 
'checkout' }),
+        expect.objectContaining({ name: 'service.namespace', type: 'query', 
value: 'payments' }),
+        expect.objectContaining({ name: 'deployment.environment.name', type: 
'query', value: 'prod' }),
+        expect.objectContaining({ name: 'hertzbeat.entity_id', type: 
'dynamic', value: '4200' }),
+        expect.objectContaining({ name: 'hertzbeat.entity_type', type: 
'dynamic', value: 'service' }),
+        expect.objectContaining({ name: 'hertzbeat.template', type: 'dynamic', 
value: 'spring-boot' })
+      ]));
+
+      const plans = buildSignalDashboardExecutionPlans(persistedDashboard);
+      const planForTitle = (titleNeedle: string) => {
+        const widget = persistedWidgets.find(item => 
item.title.includes(titleNeedle));
+        expect(widget, `service overview widget ${titleNeedle} should 
exist`).toBeTruthy();
+        return plans.find(plan => plan.panelId === widget?.id);
+      };
+      expect(plans.map(plan => 
plan.signal)).toEqual(expect.arrayContaining(['metrics', 'logs', 'traces', 
'alerts']));
+      expect(planForTitle('request rate')).toEqual(expect.objectContaining({
+        signal: 'metrics',
+        state: 'ready',
+        primaryUrl: expect.stringContaining('/ingestion/otlp/metrics/console')
+      }));
+      expect(planForTitle('request 
rate')?.primaryUrl).toEqual(expect.stringContaining('entityType=service'));
+      
expect(planForTitle('apdex')?.primaryUrl).toEqual(expect.stringContaining('template=service-apdex'));
+      expect(planForTitle('log 
errors')?.primaryUrl).toEqual(expect.stringContaining('/logs/list'));
+      expect(planForTitle('log 
errors')?.primaryUrl).toEqual(expect.stringContaining('severityText=ERROR'));
+      
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('/traces/stats/group-by'));
+      
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('groupBy=exception.type'));
+      expect(planForTitle('firing 
alerts')?.primaryUrl).toEqual(expect.stringContaining('/alerts/group'));
+      expect(planForTitle('firing 
alerts')?.primaryUrl).toEqual(expect.stringContaining('status=firing'));
+
+      const requestRatePanel = 
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText: 
'Service overview request rate' }).first();
+      await 
expect(requestRatePanel).toHaveAttribute('data-dashboard-composition-preview-signal',
 'metrics');
+      await 
expect(requestRatePanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
 /entityType=service/);
+      await 
expect(requestRatePanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
 /temporalAggregation=rate/);
+
+      const alertPanel = 
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText: 
'Service overview firing alerts' }).first();
+      await 
expect(alertPanel).toHaveAttribute('data-dashboard-composition-preview-signal', 
'alerts');
+      await 
expect(alertPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
 /\/alerts\/group\?/);
+      await 
expect(alertPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
 /status=firing/);
+    } finally {
+      await 
page.context().request.delete(`${baseUrl}/api/signal/dashboard/${encodeURIComponent(dashboardKey)}`,
 {
+        failOnStatusCode: false
+      });
+    }
+  });
+
   test('promotes logs, traces, and metrics saved views into a persisted replay 
dashboard', async ({ page }) => {
     test.setTimeout(BROWSER_SMOKE_TIMEOUT);
 
diff --git a/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts 
b/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts
index e7d78a005c..962684418c 100644
--- a/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts
+++ b/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts
@@ -24,6 +24,27 @@ describe('live dashboard source edit browser smoke 
contract', () => {
     
expect(source).toContain("expect(payload.dashboardPanelEdit).toEqual(expect.objectContaining({");
     expect(source).toContain("promotes a saved query view through real 
saved-view and panel-draft APIs");
     expect(source).toContain("promotes logs, traces, and metrics saved views 
into a persisted replay dashboard");
+    expect(source).toContain("saves a service overview dashboard from URL 
service and entity context");
+    expect(source).toContain("buildSignalServiceOverviewDashboard");
+    
expect(source).toContain("data-dashboard-service-overview-action=\"save\"");
+    
expect(source).toContain("data-dashboard-service-overview-context=\"ready\"");
+    expect(source).toContain("Checkout API service overview");
+    expect(source).toContain("expect(persistedWidgets.map(widget => 
widget.title)).toEqual(expect.arrayContaining([");
+    expect(source).toContain("'Service overview request rate: 
service.name=checkout'");
+    expect(source).toContain("'Service overview error rate: 
service.name=checkout'");
+    expect(source).toContain("'Service overview apdex: 
service.name=checkout'");
+    expect(source).toContain("'Service overview log errors: 
service.name=checkout'");
+    expect(source).toContain("'Service overview exceptions: 
service.name=checkout'");
+    expect(source).toContain("'Service overview firing alerts: 
service.name=checkout'");
+    expect(source).toContain("expect.objectContaining({ name: 
'hertzbeat.entity_id', type: 'dynamic', value: '4200' })");
+    expect(source).toContain("expect.objectContaining({ name: 
'hertzbeat.entity_type', type: 'dynamic', value: 'service' })");
+    
expect(source).toContain("buildSignalDashboardExecutionPlans(persistedDashboard)");
+    expect(source).toContain("expect(plans.map(plan => 
plan.signal)).toEqual(expect.arrayContaining(['metrics', 'logs', 'traces', 
'alerts']))");
+    expect(source).toContain("entityType=service");
+    expect(source).toContain("template=service-apdex");
+    expect(source).toContain("/logs/list");
+    expect(source).toContain("/traces/stats/group-by");
+    expect(source).toContain("/alerts/group");
     
expect(source).toContain("buildThreeSignalWorkbenchDashboardReplayExpectations");
     
expect(source).toContain("buildThreeSignalWorkbenchExpectedDashboardVariables");
     expect(source).toContain("seedReplaySavedView(page.context().request, 
expectation, proofKey)");
diff --git a/web-next/scripts/three-signal-live-proof-contract.test.ts 
b/web-next/scripts/three-signal-live-proof-contract.test.ts
index 6eecc69aa5..ee9c745f7f 100644
--- a/web-next/scripts/three-signal-live-proof-contract.test.ts
+++ b/web-next/scripts/three-signal-live-proof-contract.test.ts
@@ -17,7 +17,7 @@ describe('three-signal live proof script contract', () => {
     expect(source).toContain('TRACE_ID="${TRACE_ID}" 
HERTZBEAT_BASE="${HERTZBEAT_BASE}" bash 
script/dev/verify-otlp-three-signal-demo.sh');
     
expect(source).toContain('DASHBOARD_SOURCE_EDIT_LIVE_BROWSER_BASE_URL="${FRONTEND_BASE}"');
     expect(source).toContain('npm exec -- playwright test 
scripts/dashboard-source-edit-live-browser-smoke.spec.ts -g 
"${PLAYWRIGHT_GREP}"');
-    expect(source).toContain('promotes logs, traces, and metrics saved views 
into a persisted replay dashboard');
+    expect(source).toContain('promotes logs, traces, and metrics saved views 
into a persisted replay dashboard|saves a service overview dashboard from URL 
service and entity context');
     expect(source).toContain('trap cleanup EXIT');
     expect(source).toContain('kill "${BACKEND_PID}"');
     expect(source).toContain('kill "${FRONTEND_WRAPPER_PID}"');


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to