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 5fda65c4b0 Use response handoffs for entity signal routes
5fda65c4b0 is described below
commit 5fda65c4b0ad8aac986f0abf9483a6551913e5a9
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 20:14:44 2026 +0800
Use response handoffs for entity signal routes
---
web-next/lib/entity-detail/view-model.test.ts | 237 +++++++++++++++++++++++++-
web-next/lib/entity-detail/view-model.ts | 197 ++++++++++++++++++---
web-next/lib/types.ts | 36 ++++
3 files changed, 442 insertions(+), 28 deletions(-)
diff --git a/web-next/lib/entity-detail/view-model.test.ts
b/web-next/lib/entity-detail/view-model.test.ts
index e23536e30e..755a8ddc92 100644
--- a/web-next/lib/entity-detail/view-model.test.ts
+++ b/web-next/lib/entity-detail/view-model.test.ts
@@ -296,6 +296,182 @@ describe('entity detail view model', () => {
]);
});
+ it('lets backend response handoffs seed signal routes without overriding
inherited route context', () => {
+ const detail = {
+ entity: {
+ entity: {
+ id: 42,
+ type: 'service',
+ name: 'checkout',
+ displayName: 'Checkout',
+ environment: 'prod'
+ }
+ },
+ responseHandoffs: {
+ monitors: {
+ entityId: 42,
+ entityType: 'service',
+ entityName: 'Checkout API',
+ serviceName: 'checkout-metric',
+ serviceNamespace: 'commerce',
+ environment: 'prod',
+ start: 1713200000000,
+ end: 1713202700000,
+ source: 'monitor'
+ },
+ logs: {
+ entityId: 42,
+ entityType: 'service',
+ entityName: 'Checkout API',
+ serviceName: 'checkout-log',
+ serviceNamespace: 'commerce',
+ environment: 'prod',
+ traceId: 'trace-log',
+ spanId: 'span-log',
+ start: 1713200000000,
+ end: 1713202700000,
+ source: 'otlp'
+ },
+ traces: {
+ entityId: 42,
+ entityType: 'service',
+ entityName: 'Checkout API',
+ serviceName: 'checkout-trace',
+ serviceNamespace: 'commerce',
+ environment: 'prod',
+ traceId: 'trace-trace',
+ spanId: 'span-trace',
+ start: 1713200000000,
+ end: 1713202700000,
+ source: 'otlp'
+ }
+ }
+ } as any;
+
+ const links = buildEntityContextHandoffLinks(detail, 'last-1h');
+ const metrics = new URL(links.find(link => link.key === 'metrics')?.copy
|| '/', 'http://localhost').searchParams;
+ expect(metrics.get('entityType')).toBe('service');
+ expect(metrics.get('entityName')).toBe('Checkout API');
+ expect(metrics.get('serviceName')).toBe('checkout-metric');
+ expect(metrics.get('serviceNamespace')).toBe('commerce');
+ expect(metrics.get('start')).toBe('1713200000000');
+ expect(metrics.get('end')).toBe('1713202700000');
+ expect(metrics.get('source')).toBe('monitor');
+
+ const logs = new URL(links.find(link => link.key === 'logs')?.copy || '/',
'http://localhost').searchParams;
+ expect(logs.get('serviceName')).toBe('checkout-log');
+ expect(logs.get('traceId')).toBe('trace-log');
+ expect(logs.get('spanId')).toBe('span-log');
+ expect(logs.get('source')).toBe('otlp');
+
+ const traces = new URL(links.find(link => link.key === 'traces')?.copy ||
'/', 'http://localhost').searchParams;
+ expect(traces.get('serviceName')).toBe('checkout-trace');
+ expect(traces.get('traceId')).toBe('trace-trace');
+ expect(traces.get('spanId')).toBe('span-trace');
+
+ const inheritedLinks = buildEntityContextHandoffLinks(detail, {
+ entityName: 'Route Checkout',
+ serviceName: 'route-service',
+ serviceNamespace: 'route-namespace',
+ environment: 'route-env',
+ traceId: 'route-trace',
+ spanId: 'route-span',
+ start: '10',
+ end: '20',
+ source: 'route-source'
+ });
+ const inheritedLogs = new URL(inheritedLinks.find(link => link.key ===
'logs')?.copy || '/', 'http://localhost').searchParams;
+ expect(inheritedLogs.get('entityName')).toBe('Route Checkout');
+ expect(inheritedLogs.get('serviceName')).toBe('route-service');
+ expect(inheritedLogs.get('serviceNamespace')).toBe('route-namespace');
+ expect(inheritedLogs.get('environment')).toBe('route-env');
+ expect(inheritedLogs.get('traceId')).toBe('route-trace');
+ expect(inheritedLogs.get('spanId')).toBe('route-span');
+ expect(inheritedLogs.get('start')).toBe('10');
+ expect(inheritedLogs.get('end')).toBe('20');
+ expect(inheritedLogs.get('source')).toBe('route-source');
+ });
+
+ it('builds host entity handoff links with resource filters instead of
inventing a service name', () => {
+ const links = buildEntityContextHandoffLinks(
+ {
+ entity: {
+ entity: {
+ id: 4201,
+ name: 'checkout-node-a',
+ displayName: 'Checkout Node A',
+ type: 'host'
+ },
+ identities: [{ key: 'host.name', value: 'checkout-node-a' }]
+ }
+ } as any,
+ 'last-30m'
+ );
+
+ const metrics = new URL(links[0]?.copy || '/', 'http://localhost');
+ expect(metrics.pathname).toBe('/ingestion/otlp/metrics');
+ expect(metrics.searchParams.get('entityId')).toBe('4201');
+ expect(metrics.searchParams.get('serviceName')).toBeNull();
+
expect(metrics.searchParams.get('filter')).toBe('host.name="checkout-node-a"');
+
+ const logs = new URL(links[1]?.copy || '/', 'http://localhost');
+ expect(logs.pathname).toBe('/log/manage');
+ expect(logs.searchParams.get('serviceName')).toBeNull();
+
expect(logs.searchParams.get('resourceFilter')).toBe('host.name="checkout-node-a"');
+
+ const traces = new URL(links[2]?.copy || '/', 'http://localhost');
+ expect(traces.pathname).toBe('/trace/manage');
+ expect(traces.searchParams.get('serviceName')).toBeNull();
+
expect(traces.searchParams.get('resourceFilter')).toBe('host.name="checkout-node-a"');
+ });
+
+ it('builds k8s workload handoff links with inherited service context and pod
resource filters', () => {
+ const links = buildEntityContextHandoffLinks(
+ {
+ entity: {
+ entity: {
+ id: 4202,
+ name: 'checkout-v1-78dfd',
+ displayName: 'Checkout Pod',
+ type: 'k8s_workload',
+ namespace: 'payments'
+ },
+ identities: [
+ { key: 'k8s.namespace.name', value: 'payments' },
+ { key: 'k8s.pod.name', value: 'checkout-v1-78dfd' },
+ { key: 'container.name', value: 'checkout' }
+ ]
+ }
+ } as any,
+ {
+ timeRange: 'last-45m',
+ source: 'otlp',
+ collector: 'collector-demo-a',
+ template: 'spring-boot',
+ serviceName: 'checkout',
+ serviceNamespace: 'hertzbeat-demo',
+ environment: 'demo',
+ returnTo: '/entities/4200'
+ }
+ );
+ const expectedFilter = 'k8s.namespace.name="payments" and
k8s.pod.name="checkout-v1-78dfd" and container.name="checkout"';
+
+ const metrics = new URL(links[0]?.copy || '/', 'http://localhost');
+ expect(metrics.searchParams.get('entityId')).toBe('4202');
+ expect(metrics.searchParams.get('serviceName')).toBe('checkout');
+
expect(metrics.searchParams.get('serviceNamespace')).toBe('hertzbeat-demo');
+ expect(metrics.searchParams.get('source')).toBe('otlp');
+ expect(metrics.searchParams.get('filter')).toBe(expectedFilter);
+
+ const logs = new URL(links[1]?.copy || '/', 'http://localhost');
+ expect(logs.searchParams.get('resourceFilter')).toBe(expectedFilter);
+ expect(logs.searchParams.get('collector')).toBe('collector-demo-a');
+
+ const traces = new URL(links[2]?.copy || '/', 'http://localhost');
+ expect(traces.searchParams.get('resourceFilter')).toBe(expectedFilter);
+ expect(traces.searchParams.get('template')).toBe('spring-boot');
+ });
+
it('builds alert, topology, and runbook handoffs only from real entity
evidence', () => {
const rows = buildEntityEvidenceHandoffRows(
{
@@ -491,7 +667,12 @@ describe('entity detail view model', () => {
{ title: 'Current alert #1', copy: 'error rate high', meta: 'firing ยท
severity=critical', tone: 'danger' }
]);
expect(buildRelationshipRows(detail)).toEqual([
- { title: 'calls', copy: 'mysql-prod', meta: 'mysql-1' },
+ {
+ title: 'calls',
+ copy: 'mysql-prod',
+ meta: 'mysql-1',
+ href:
'/entities/mysql-1?entityId=mysql-1&entityName=mysql-prod&timeRange=last-1h'
+ },
{ title: 'owned-by', copy: 'payment-app', meta: 'Upstream/downstream
relationship' }
]);
expect(buildCollectionSourceRows(detail)).toEqual([
@@ -502,6 +683,60 @@ describe('entity detail view model', () => {
]);
});
+ it('keeps OTLP entity relation drilldowns linked to target entities with
signal context', () => {
+ const detail = {
+ entity: {
+ entity: {
+ id: 4200,
+ name: 'checkout',
+ displayName: 'Checkout API',
+ source: 'otlp'
+ },
+ relations: [
+ {
+ relationType: 'runs_on',
+ targetEntityId: 4201,
+ targetRef: 'host:checkout-node-a',
+ relationSource: 'otel_resource',
+ status: 'confirmed'
+ },
+ {
+ relationType: 'deployed_on',
+ targetEntityId: 4202,
+ targetRef: 'k8s_workload:payments/checkout-v1-78dfd',
+ relationSource: 'otel_resource',
+ status: 'confirmed'
+ }
+ ]
+ }
+ } as any;
+
+ expect(
+ buildRelationshipRows(detail, {
+ timeRange: 'last-45m',
+ source: 'otlp',
+ collector: 'collector-demo-a',
+ template: 'spring-boot',
+ serviceName: 'checkout',
+ serviceNamespace: 'hertzbeat-demo',
+ environment: 'demo'
+ })
+ ).toEqual([
+ {
+ title: 'runs_on',
+ copy: 'host:checkout-node-a',
+ meta: '4201',
+ href:
'/entities/4201?entityId=4201&entityName=host%3Acheckout-node-a&serviceName=checkout&environment=demo&timeRange=last-45m&source=otlp&collector=collector-demo-a&template=spring-boot&serviceNamespace=hertzbeat-demo&returnTo=%2Fentities%2F4200'
+ },
+ {
+ title: 'deployed_on',
+ copy: 'k8s_workload:payments/checkout-v1-78dfd',
+ meta: '4202',
+ href:
'/entities/4202?entityId=4202&entityName=k8s_workload%3Apayments%2Fcheckout-v1-78dfd&serviceName=checkout&environment=demo&timeRange=last-45m&source=otlp&collector=collector-demo-a&template=spring-boot&serviceNamespace=hertzbeat-demo&returnTo=%2Fentities%2F4200'
+ }
+ ]);
+ });
+
it('builds entity attribution rows across traditional monitoring and OTLP
evidence', () => {
const detail = {
entity: {
diff --git a/web-next/lib/entity-detail/view-model.ts
b/web-next/lib/entity-detail/view-model.ts
index bf7e0f55a8..694f19a32c 100644
--- a/web-next/lib/entity-detail/view-model.ts
+++ b/web-next/lib/entity-detail/view-model.ts
@@ -1,4 +1,4 @@
-import type { Entity, EntityDetailDto, EntityLinkRef } from '@/lib/types';
+import type { Entity, EntityDetailDto, EntityLinkRef,
EntityResponseHandoffInfo } from '@/lib/types';
import { buildCollectorHealthEvidence } from '../collector-health-evidence';
import { interpolate, type TranslationParams } from '../i18n';
import { SUPPLEMENTAL_MESSAGES } from '../i18n-runtime-messages';
@@ -37,6 +37,13 @@ type HandoffRow = DetailRow & {
key: string;
};
+type SignalHandoffKind = 'metrics' | 'logs' | 'traces';
+
+type EntityResourceScope = {
+ metricsFilter: string;
+ resourceFilter: string;
+};
+
type EvidenceHandoffRow = DetailRow & {
key: 'alerts' | 'topology' | 'runbook';
evidence: 'active-alerts' | 'topology-relation' | 'runbook';
@@ -178,7 +185,7 @@ function localizeActionText(
export function buildSummaryRows(
detail: Pick<EntityDetailDto, 'evidenceSummary' | 'monitorSummary' |
'logSummary' | 'traceSummary' | 'signalEvidence' | 'boundMonitors'>,
t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
-) {
+): DetailRow[] {
const boundMonitorCount = detail.monitorSummary?.totalBoundMonitors ??
detail.boundMonitors?.length ?? 0;
const downMonitorCount = detail.evidenceSummary?.downMonitorCount ?? 0;
const logSummary = getSignalLogSummary(detail);
@@ -542,6 +549,83 @@ function normalizeUnknownText(value: unknown) {
return undefined;
}
+function responseHandoffForSignal(detail: EntityDetailDto, signal?:
SignalHandoffKind): EntityResponseHandoffInfo | null {
+ if (signal === 'metrics') return detail.responseHandoffs?.monitors || null;
+ if (signal === 'logs') return detail.responseHandoffs?.logs || null;
+ if (signal === 'traces') return detail.responseHandoffs?.traces || null;
+ return null;
+}
+
+function normalizeEntityType(entity: Entity) {
+ return normalizeUnknownText(entity.type)?.toLowerCase().replaceAll('-', '_');
+}
+
+function isKnownResourceEntity(entity: Entity) {
+ const type = normalizeEntityType(entity);
+ return type === 'host' || type === 'k8s_workload' || type === 'k8s_pod' ||
type === 'kubernetes_workload';
+}
+
+function collectEntityIdentityValues(detail: EntityDetailDto) {
+ const values = new Map<string, string>();
+ const put = (key: unknown, value: unknown) => {
+ const normalizedKey = normalizeUnknownText(key);
+ const normalizedValue = normalizeUnknownText(value);
+ if (normalizedKey && normalizedValue && !values.has(normalizedKey)) {
+ values.set(normalizedKey, normalizedValue);
+ }
+ };
+
+ (detail.entity?.identities || []).forEach(identity => {
+ if (!identity || typeof identity !== 'object') return;
+ const record = identity as Record<string, unknown>;
+ put(record.key || record.name || record.identityKey, record.value ||
record.identityValue);
+ });
+
+ const labels = detail.entity?.entity?.labels;
+ if (labels && typeof labels === 'object' && !Array.isArray(labels)) {
+ Object.entries(labels as Record<string, unknown>).forEach(([key, value])
=> put(key, value));
+ }
+
+ return values;
+}
+
+function quoteResourceFilterValue(value: string) {
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
+}
+
+function buildResourceFilterExpression(pairs: Array<[string, string |
undefined]>) {
+ return pairs
+ .map(([key, value]) => (value ?
`${key}=${quoteResourceFilterValue(value)}` : undefined))
+ .filter((value): value is string => Boolean(value))
+ .join(' and ');
+}
+
+function buildEntityResourceScope(detail: EntityDetailDto):
EntityResourceScope | null {
+ const entity = getEntityRecord(detail);
+ const type = normalizeEntityType(entity);
+ const identityValues = collectEntityIdentityValues(detail);
+
+ if (type === 'host') {
+ const hostName = identityValues.get('host.name') ||
normalizeUnknownText(entity.name);
+ const filter = buildResourceFilterExpression([['host.name', hostName]]);
+ return filter ? { metricsFilter: filter, resourceFilter: filter } : null;
+ }
+
+ if (type === 'k8s_workload' || type === 'k8s_pod' || type ===
'kubernetes_workload') {
+ const namespace = identityValues.get('k8s.namespace.name') ||
normalizeUnknownText(entity.namespace);
+ const pod = identityValues.get('k8s.pod.name') ||
normalizeUnknownText(entity.name);
+ const container = identityValues.get('container.name');
+ const filter = buildResourceFilterExpression([
+ ['k8s.namespace.name', namespace],
+ ['k8s.pod.name', pod],
+ ['container.name', container]
+ ]);
+ return filter ? { metricsFilter: filter, resourceFilter: filter } : null;
+ }
+
+ return null;
+}
+
function resolveEntityTimeContext(context: EntityDetailTimeContext |
undefined): SignalRouteContext {
if (!context) return { timeRange: 'last-1h' };
return typeof context === 'string' ? { timeRange: context } : context;
@@ -579,29 +663,55 @@ function appendPreservedEntityContext(params:
URLSearchParams, routeContext: Sig
});
}
+function appendResponseHandoffContext(params: URLSearchParams, handoff:
EntityResponseHandoffInfo | null) {
+ if (!handoff) return;
+ addContextParam(params, 'start', normalizeUnknownText(handoff.start));
+ addContextParam(params, 'end', normalizeUnknownText(handoff.end));
+ addContextParam(params, 'source', normalizeUnknownText(handoff.source));
+}
+
function buildContextParams(
detail: EntityDetailDto,
timeContext: EntityDetailTimeContext,
- options: { includeTrace?: boolean; includeEntityName?: boolean;
includeReturnTo?: boolean } = {}
+ options: { includeTrace?: boolean; includeEntityName?: boolean;
includeReturnTo?: boolean; signal?: SignalHandoffKind } = {}
) {
const entity = getEntityRecord(detail);
const routeContext = resolveEntityTimeContext(timeContext);
- const entityId = entity.id != null ? String(entity.id) : '';
- const entityName = entity.displayName || entity.name || '';
- const serviceName = normalizeContextText(routeContext.serviceName) ||
entity.name || entity.displayName || '';
+ const responseHandoff = responseHandoffForSignal(detail, options.signal);
+ const responseEntityName = normalizeUnknownText(responseHandoff?.entityName);
+ const routeEntityName = normalizeContextText(routeContext.entityName);
+ const entityId = normalizeContextText(routeContext.entityId) ||
normalizeUnknownText(responseHandoff?.entityId) || (entity.id != null ?
String(entity.id) : '');
+ const entityName = routeEntityName || responseEntityName ||
entity.displayName || entity.name || '';
+ const entityType = normalizeContextText(routeContext.entityType) ||
normalizeUnknownText(responseHandoff?.entityType) ||
normalizeEntityType(entity);
+ const resourceScope = buildEntityResourceScope(detail);
+ const serviceName =
+ normalizeContextText(routeContext.serviceName) ||
+ normalizeUnknownText(responseHandoff?.serviceName) ||
+ (isKnownResourceEntity(entity) ? '' : entity.name || entity.displayName ||
'');
+ const serviceNamespace = normalizeContextText(routeContext.serviceNamespace)
|| normalizeUnknownText(responseHandoff?.serviceNamespace);
+ const environment = normalizeContextText(routeContext.environment) ||
normalizeUnknownText(responseHandoff?.environment) || entity.environment;
const traceSummary = getSignalTraceSummary(detail);
const timeRange = normalizeContextText(routeContext.timeRange) || 'last-1h';
const params = new URLSearchParams();
addContextParam(params, 'entityId', entityId);
- if (options.includeEntityName) addContextParam(params, 'entityName',
entityName);
+ addContextParam(params, 'entityType', entityType);
+ if (options.includeEntityName || routeEntityName || responseEntityName)
addContextParam(params, 'entityName', entityName);
addContextParam(params, 'serviceName', serviceName);
- addContextParam(params, 'environment',
normalizeContextText(routeContext.environment) || entity.environment);
+ addContextParam(params, 'serviceNamespace', serviceNamespace);
+ addContextParam(params, 'environment', environment);
addContextParam(params, 'timeRange', timeRange);
+ appendResponseHandoffContext(params, responseHandoff);
appendPreservedEntityContext(params, routeContext);
+ if (options.signal === 'metrics' && resourceScope?.metricsFilter) {
+ addContextParam(params, 'filter', resourceScope.metricsFilter);
+ }
+ if ((options.signal === 'logs' || options.signal === 'traces') &&
resourceScope?.resourceFilter) {
+ addContextParam(params, 'resourceFilter', resourceScope.resourceFilter);
+ }
if (options.includeTrace) {
- addContextParam(params, 'traceId',
normalizeContextText(routeContext.traceId) || traceSummary?.latestTraceId);
- addContextParam(params, 'spanId',
normalizeContextText(routeContext.spanId) || traceSummary?.latestSpanId);
+ addContextParam(params, 'traceId',
normalizeContextText(routeContext.traceId) ||
normalizeUnknownText(responseHandoff?.traceId) || traceSummary?.latestTraceId);
+ addContextParam(params, 'spanId',
normalizeContextText(routeContext.spanId) ||
normalizeUnknownText(responseHandoff?.spanId) || traceSummary?.latestSpanId);
}
if (options.includeReturnTo && entityId) {
params.set('returnTo', `/entities/${entityId}`);
@@ -622,26 +732,28 @@ export function buildEntityContextHandoffLinks(
const entity = getEntityRecord(detail);
const entityId = entity.id != null ? String(entity.id) : '';
const shared = buildContextParams(detail, timeContext);
- const sharedWithTrace = buildContextParams(detail, timeContext, {
includeTrace: true });
+ const metricsQuery = buildContextParams(detail, timeContext, { signal:
'metrics' });
+ const logsQuery = buildContextParams(detail, timeContext, { includeTrace:
true, signal: 'logs' });
+ const tracesQuery = buildContextParams(detail, timeContext, { includeTrace:
true, signal: 'traces' });
const monitorQuery = buildContextParams(detail, timeContext, {
includeEntityName: true, includeReturnTo: true });
return [
{
key: 'metrics',
title: t('entities.detail.handoff.metrics.title'),
- copy: withQuery('/ingestion/otlp/metrics', shared),
+ copy: withQuery('/ingestion/otlp/metrics', metricsQuery),
meta: t('entities.detail.handoff.metrics.meta')
},
{
key: 'logs',
title: t('entities.detail.handoff.logs.title'),
- copy: withQuery('/log/manage', sharedWithTrace),
+ copy: withQuery('/log/manage', logsQuery),
meta: t('entities.detail.handoff.logs.meta')
},
{
key: 'traces',
title: t('entities.detail.handoff.traces.title'),
- copy: withQuery('/trace/manage', sharedWithTrace),
+ copy: withQuery('/trace/manage', tracesQuery),
meta: t('entities.detail.handoff.traces.meta')
},
{
@@ -809,6 +921,7 @@ export function buildCurrentAlertRows(
export function buildRelationshipRows(
detail: EntityDetailDto,
+ timeContext: EntityDetailTimeContext = 'last-1h',
t: EntityDetailViewModelTranslator = translateEntityDetailViewModel
): DetailRow[] {
const relations = (detail.entity?.relations || []) as Array<Record<string,
unknown>>;
@@ -823,19 +936,49 @@ export function buildRelationshipRows(
];
}
- return relations.map(relation => ({
- title: String(relation.type || relation.relationType || 'related'),
- copy: String(
- relation.targetEntityName ||
- relation.targetName ||
- relation.targetEntityId ||
- t('entities.detail.relationship-row.unknown-target')
- ),
- meta:
- relation.targetEntityId != null
- ? String(relation.targetEntityId)
- : t('entities.detail.relationship-row.default-meta')
- }));
+ const sourceEntity = getEntityRecord(detail);
+ const sourceEntityId = normalizeUnknownText(sourceEntity.id);
+ const routeContext = resolveEntityTimeContext(timeContext);
+ const timeRange = normalizeContextText(routeContext.timeRange) || 'last-1h';
+
+ return relations.map(relation => {
+ const targetEntityId = normalizeUnknownText(relation.targetEntityId) ||
normalizeUnknownText(relation.targetId);
+ const targetEntityName =
+ normalizeUnknownText(relation.targetEntityName) ||
+ normalizeUnknownText(relation.targetName) ||
+ normalizeUnknownText(relation.targetRef) ||
+ targetEntityId ||
+ t('entities.detail.relationship-row.unknown-target');
+ const params = new URLSearchParams();
+
+ if (targetEntityId) {
+ addContextParam(params, 'entityId', targetEntityId);
+ addContextParam(params, 'entityName', targetEntityName);
+ addContextParam(params, 'serviceName',
normalizeContextText(routeContext.serviceName));
+ addContextParam(params, 'environment',
normalizeContextText(routeContext.environment));
+ addContextParam(params, 'timeRange', timeRange);
+ appendPreservedEntityContext(params, routeContext);
+ if (sourceEntityId) {
+ params.set('returnTo', `/entities/${sourceEntityId}`);
+ }
+ }
+
+ return {
+ title: String(relation.type || relation.relationType || 'related'),
+ copy: String(
+ relation.targetEntityName ||
+ relation.targetName ||
+ relation.targetRef ||
+ relation.targetEntityId ||
+ t('entities.detail.relationship-row.unknown-target')
+ ),
+ meta:
+ relation.targetEntityId != null
+ ? String(relation.targetEntityId)
+ : t('entities.detail.relationship-row.default-meta'),
+ href: targetEntityId ? withQuery(`/entities/${targetEntityId}`,
params.toString()) : undefined
+ };
+ });
}
function identitySummary(identities: unknown[]) {
diff --git a/web-next/lib/types.ts b/web-next/lib/types.ts
index a04f05c94d..00636d3716 100644
--- a/web-next/lib/types.ts
+++ b/web-next/lib/types.ts
@@ -534,12 +534,48 @@ export interface EntityDetailDto {
traceSummary?: EntityTraceSummary & { latestSpanId?: string | null };
unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary;
signalEvidence?: EntitySignalEvidenceBundle;
+ responseHandoffs?: EntityResponseHandoffsInfo;
boundMonitors?: Monitor[];
activeAlerts?: unknown[];
nextActions?: EntityNextAction[];
noiseControlSummary?: EntityNoiseControlSummary;
}
+export interface EntityResponseHandoffInfo {
+ search?: string | null;
+ status?: string | null;
+ severity?: string | null;
+ app?: string | null;
+ content?: string | null;
+ entityId?: number | string | null;
+ entityType?: string | null;
+ entityName?: string | null;
+ traceId?: string | null;
+ spanId?: string | null;
+ serviceName?: string | null;
+ serviceNamespace?: string | null;
+ severityText?: string | null;
+ query?: string | null;
+ owner?: string | null;
+ system?: string | null;
+ environment?: string | null;
+ start?: number | string | null;
+ end?: number | string | null;
+ source?: string | null;
+ focus?: string | null;
+ returnTo?: string | null;
+ returnLabel?: string | null;
+}
+
+export interface EntityResponseHandoffsInfo {
+ alerts?: EntityResponseHandoffInfo | null;
+ monitors?: EntityResponseHandoffInfo | null;
+ logs?: EntityResponseHandoffInfo | null;
+ traces?: EntityResponseHandoffInfo | null;
+ discovery?: EntityResponseHandoffInfo | null;
+ editor?: EntityResponseHandoffInfo | null;
+}
+
export interface EntitySignalEvidenceBundle {
logSummary?: EntityLogSummary;
traceSummary?: EntityTraceSummary & { latestSpanId?: string | null };
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]