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 62941f3322 Add operation drilldown live proof
62941f3322 is described below

commit 62941f3322a58abce4de883ee5c0a336881d548d
Author: Logic <[email protected]>
AuthorDate: Mon Jun 8 10:56:30 2026 +0800

    Add operation drilldown live proof
---
 script/dev/run-three-signal-live-proof.sh          |   4 +-
 ...ashboard-source-edit-live-browser-smoke.spec.ts | 134 +++++++++++++++++++++
 ...ashboard-source-edit-live-browser-smoke.test.ts |  11 ++
 .../three-signal-live-proof-contract.test.ts       |   3 +-
 4 files changed, 149 insertions(+), 3 deletions(-)

diff --git a/script/dev/run-three-signal-live-proof.sh 
b/script/dev/run-three-signal-live-proof.sh
index 5976d884e4..2b0861f9f1 100755
--- a/script/dev/run-three-signal-live-proof.sh
+++ b/script/dev/run-three-signal-live-proof.sh
@@ -15,9 +15,9 @@ 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|saves a service overview dashboard from 
URL service and entity context}"
+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|saves an operation drilldown dashboard from URL 
operation context}"
 BACKEND_READY_PATH="${BACKEND_READY_PATH:-/actuator/health}"
-READY_ATTEMPTS="${READY_ATTEMPTS:-90}"
+READY_ATTEMPTS="${READY_ATTEMPTS:-300}"
 READY_SLEEP_SECONDS="${READY_SLEEP_SECONDS:-2}"
 
 DRY_RUN=false
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 a214d54b57..bdb9eed634 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,
+  buildSignalOperationDrilldownDashboard,
   buildSignalServiceOverviewDashboard,
   createSignalDashboardPanelDraftFromRuntimeBreakout,
   createSignalDashboardPanelDraftFromRuntimeEvidence,
@@ -174,6 +175,24 @@ function serviceOverviewRoute() {
   ].join('&');
 }
 
+function operationDrilldownRoute() {
+  return [
+    '/dashboard?serviceName=checkout',
+    'serviceNamespace=payments',
+    'environment=prod',
+    'operationName=POST%20%2Fcheckout',
+    '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';
@@ -600,6 +619,121 @@ test.describe('live dashboard source edit browser smoke', 
() => {
     }
   });
 
+  test('saves an operation drilldown dashboard from URL operation context', 
async ({ page }) => {
+    test.setTimeout(BROWSER_SMOKE_TIMEOUT);
+
+    const expectedDashboard = buildSignalOperationDrilldownDashboard({
+      serviceName: 'checkout',
+      serviceNamespace: 'payments',
+      environment: 'prod',
+      operationName: 'POST /checkout',
+      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}${operationDrilldownRoute()}`, {
+        timeout: BROWSER_SMOKE_TIMEOUT,
+        waitUntil: 'domcontentloaded'
+      });
+
+      const operationDrilldownAction = 
page.locator('[data-dashboard-operation-drilldown-action="save"]').first();
+      await 
expect(page.locator('[data-dashboard-operation-drilldown-context="ready"]')).toHaveAttribute(
+        'data-dashboard-operation-drilldown-operation',
+        'POST /checkout',
+        { timeout: WORKBENCH_READY_TIMEOUT }
+      );
+      await 
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-state',
 'ready');
+      await 
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-service',
 'checkout');
+      await 
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-operation',
 'POST /checkout');
+      await operationDrilldownAction.click();
+
+      await 
expect(page).toHaveURL(/dashboard=service-checkout-operation-post-checkout-drilldown/,
 {
+        timeout: WORKBENCH_READY_TIMEOUT
+      });
+      await 
expect(page.locator('[data-dashboard-composition-preview-panel]')).toHaveCount(8,
 {
+        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(8);
+
+      const persistedDashboard = await loadDashboard(page.context().request, 
dashboardKey);
+      const persistedWidgets = parseWidgets(persistedDashboard);
+      const persistedVariables = parseVariables(persistedDashboard);
+      expect(persistedDashboard).toEqual(expect.objectContaining({
+        dashboardKey: 'service-checkout-operation-post-checkout-drilldown',
+        title: 'Checkout API POST /checkout operation drilldown',
+        tags: 'service,operation,apm,metrics,logs,traces'
+      }));
+      expect(persistedWidgets.map(widget => widget.title)).toEqual([
+        'Operation drilldown latency p95: operation.name=POST /checkout',
+        'Operation drilldown request rate: operation.name=POST /checkout',
+        'Operation drilldown error rate: operation.name=POST /checkout',
+        'Operation drilldown logs: operation.name=POST /checkout',
+        'Operation drilldown log errors: operation.name=POST /checkout',
+        'Operation drilldown traces: operation.name=POST /checkout',
+        'Operation drilldown trace errors: operation.name=POST /checkout',
+        'Operation drilldown exceptions: operation.name=POST /checkout'
+      ]);
+      expect(persistedVariables).toEqual(expect.arrayContaining([
+        expect.objectContaining({ name: 'operation.name', type: 'query', 
value: 'POST /checkout' }),
+        expect.objectContaining({ name: 'service.name', type: 'query', value: 
'checkout' }),
+        expect.objectContaining({ name: 'service.namespace', type: 'query', 
value: 'payments' }),
+        expect.objectContaining({ name: 'hertzbeat.entity_id', type: 
'dynamic', value: '4200' }),
+        expect.objectContaining({ name: 'hertzbeat.entity_type', type: 
'dynamic', value: 'service' })
+      ]));
+
+      const plans = buildSignalDashboardExecutionPlans(persistedDashboard);
+      const planForTitle = (titleNeedle: string) => {
+        const widget = persistedWidgets.find(item => 
item.title.includes(titleNeedle));
+        expect(widget, `operation drilldown 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']));
+      expect(planForTitle('latency p95')).toEqual(expect.objectContaining({
+        signal: 'metrics',
+        state: 'ready',
+        primaryUrl: 
expect.stringContaining('operation%3D%22POST+%2Fcheckout%22')
+      }));
+      
expect(planForTitle('logs')?.primaryUrl).toEqual(expect.stringContaining('/logs/list'));
+      
expect(planForTitle('logs')?.primaryUrl).toEqual(expect.stringContaining('attributeFilter=http.route%3APOST+%2Fcheckout'));
+      
expect(planForTitle('traces')?.primaryUrl).toEqual(expect.stringContaining('/traces/list'));
+      
expect(planForTitle('traces')?.primaryUrl).toEqual(expect.stringContaining('operationName=POST+%2Fcheckout'));
+      
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('/traces/stats/group-by'));
+      
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('groupBy=exception.type'));
+
+      const latencyPanel = 
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText: 
'Operation drilldown latency p95' }).first();
+      await 
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
 'metrics');
+      await 
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
 /operation%3D%22POST\+%2Fcheckout%22/);
+      await 
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
 /entityType=service/);
+
+      const tracesPanel = 
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText: 
'Operation drilldown traces' }).first();
+      await 
expect(tracesPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
 'traces');
+      await 
expect(tracesPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
 /operationName=POST\+%2Fcheckout/);
+    } 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 962684418c..e89f43012c 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
@@ -26,6 +26,8 @@ describe('live dashboard source edit browser smoke contract', 
() => {
     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("saves an operation drilldown dashboard from URL 
operation context");
+    expect(source).toContain("buildSignalOperationDrilldownDashboard");
     
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");
@@ -36,6 +38,15 @@ describe('live dashboard source edit browser smoke 
contract', () => {
     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("data-dashboard-operation-drilldown-action=\"save\"");
+    
expect(source).toContain("data-dashboard-operation-drilldown-context=\"ready\"");
+    
expect(source).toContain("service-checkout-operation-post-checkout-drilldown");
+    expect(source).toContain("Checkout API POST /checkout operation 
drilldown");
+    expect(source).toContain("'Operation drilldown latency p95: 
operation.name=POST /checkout'");
+    expect(source).toContain("'Operation drilldown traces: operation.name=POST 
/checkout'");
+    expect(source).toContain("operation%3D%22POST+%2Fcheckout%22");
+    expect(source).toContain("attributeFilter=http.route%3APOST+%2Fcheckout");
+    expect(source).toContain("operationName=POST+%2Fcheckout");
     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)");
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 ee9c745f7f..ca84696d82 100644
--- a/web-next/scripts/three-signal-live-proof-contract.test.ts
+++ b/web-next/scripts/three-signal-live-proof-contract.test.ts
@@ -11,13 +11,14 @@ describe('three-signal live proof script contract', () => {
     expect(source).toContain('script/dev/start-workspace-backend.sh');
     expect(source).toContain('script/dev/start-mixed-frontend.sh');
     
expect(source).toContain('BACKEND_READY_PATH="${BACKEND_READY_PATH:-/actuator/health}"');
+    expect(source).toContain('READY_ATTEMPTS="${READY_ATTEMPTS:-300}"');
     expect(source).toContain('"readyPath": "${BACKEND_READY_PATH}"');
     expect(source).toContain('wait_for_http 
"${HERTZBEAT_BASE}${BACKEND_READY_PATH}"');
     expect(source).toContain('|| "${status}" == "401"');
     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|saves a service overview dashboard from URL 
service and entity context');
+    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|saves an operation drilldown dashboard from URL 
operation 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