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

wu-sheng pushed a commit to branch feat/explore
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git

commit 9d399a1c965ac082924dbc42be06865d1c09acf2
Author: Wu Sheng <[email protected]>
AuthorDate: Tue Jun 23 15:01:07 2026 +0800

    feat(explore): cross-layer trace inspect — native source (Increment 1)
    
    New /operate/explore power-tool: query traces for ANY service from one
    place, no layer nav. The entity is PICKED (layer-filtered dropdowns) or
    TYPED (service name + real flag), the latter encoded BFF-side into the
    OAP id (base64(name).{1|0}) so the query needs no layer and no
    listServices round-trip. Dispatches through POST /api/explore/query onto
    the existing native trace fetch (queryTraces/queryBasicTraces auto-
    detected); results render with the reused NativeTraceWaterfall, plus a
    resolved-query panel. Gated on a new data-tier verb explore:read.
    
    Zipkin trace + log (raw/browser) sources land next on the same spine.
---
 apps/bff/src/config/schema.ts                      |   3 +
 apps/bff/src/http/query/explore.ts                 | 170 +++++++
 apps/bff/src/http/query/trace.ts                   |   6 +-
 apps/bff/src/rbac/route-policy.ts                  |   2 +
 apps/bff/src/server.ts                             |   2 +
 apps/bff/src/util/entityId.ts                      |  50 ++
 apps/ui/src/api/client.ts                          |  10 +
 apps/ui/src/api/scopes/explore.ts                  |  28 ++
 .../src/features/operate/explore/ExploreView.vue   | 535 +++++++++++++++++++++
 apps/ui/src/shell/AppSidebar.vue                   |   1 +
 apps/ui/src/shell/router/index.ts                  |  10 +
 packages/api-client/src/explore.ts                 | 115 +++++
 packages/api-client/src/index.ts                   |  10 +
 13 files changed, 939 insertions(+), 3 deletions(-)

diff --git a/apps/bff/src/config/schema.ts b/apps/bff/src/config/schema.ts
index c220d3b..b09ed25 100644
--- a/apps/bff/src/config/schema.ts
+++ b/apps/bff/src/config/schema.ts
@@ -175,6 +175,7 @@ const rbacSchema = z
           'traces:read',
           'logs:read',
           'browser-errors:read',
+          'explore:read',
           'topology:read',
           'profile:read',
           'overview:read',
@@ -189,6 +190,7 @@ const rbacSchema = z
           'traces:read',
           'logs:read',
           'browser-errors:read',
+          'explore:read',
           'topology:read',
           'profile:read',
           'overview:read',
@@ -207,6 +209,7 @@ const rbacSchema = z
           'traces:read',
           'logs:read',
           'browser-errors:read',
+          'explore:read',
           'source-map:write',
           'topology:read',
           'profile:read',
diff --git a/apps/bff/src/http/query/explore.ts 
b/apps/bff/src/http/query/explore.ts
new file mode 100644
index 0000000..f2d8e14
--- /dev/null
+++ b/apps/bff/src/http/query/explore.ts
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Explore — cross-layer trace/log query.
+ *
+ *   POST /api/explore/query
+ *
+ * Dispatches an {@link ExploreRequest} by kind+source onto the SAME query
+ * logic the per-layer Traces/Logs tabs use, but with NO layer: the entity
+ * arrives either as pre-resolved OAP ids (Pick mode) or as a name + the
+ * real/normal flag (Type mode), which we encode here. Trace detail reuses
+ * the existing GET /api/trace/:traceId route — no detail route here.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type {
+  ExploreEntity,
+  ExploreRequest,
+  ExploreResolved,
+  ExploreResponse,
+  ExploreWindow,
+  FetchLike,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { buildOapOpts } from '../../client/graphql.js';
+import { getServerOffsetMinutes } from '../../util/window.js';
+import { buildEndpointId, buildInstanceId, buildServiceId } from 
'../../util/entityId.js';
+import { fetchNativeList, fetchZipkinList, type TraceListBody } from 
'./trace.js';
+
+export interface ExploreRouteDeps {
+  config: ConfigSource;
+  sessions: SessionStore;
+  fetch?: FetchLike;
+}
+
+/** Resolve the entity to the ids the native trace/log queries take. Pick
+ *  mode forwards the ids the metadata routes minted; Type mode encodes
+ *  base64(name).{1|0} (+ nested instance/endpoint) — no layer needed. */
+function resolveNativeEntity(e: ExploreEntity): {
+  serviceId?: string;
+  instanceId?: string;
+  endpointId?: string;
+} {
+  const serviceId =
+    e.serviceId ?? (e.serviceName ? buildServiceId(e.serviceName, e.isReal ?? 
true) : undefined);
+  const instanceId =
+    e.instanceId ?? (serviceId && e.instanceName ? buildInstanceId(serviceId, 
e.instanceName) : undefined);
+  const endpointId =
+    e.endpointId ?? (serviceId && e.endpointName ? buildEndpointId(serviceId, 
e.endpointName) : undefined);
+  return { serviceId, instanceId, endpointId };
+}
+
+/** Explicit epoch-ms window overrides the rolling minutes; trace.ts reads
+ *  `start`/`end` as ISO and `windowMinutes` as the fallback. */
+function traceWindowFields(w: ExploreWindow): Pick<TraceListBody, 
'windowMinutes' | 'start' | 'end'> {
+  if (typeof w.startMs === 'number' && typeof w.endMs === 'number' && w.endMs 
> w.startMs) {
+    return { start: new Date(w.startMs).toISOString(), end: new 
Date(w.endMs).toISOString() };
+  }
+  return { windowMinutes: w.windowMinutes };
+}
+
+export function registerExploreRoutes(app: FastifyInstance, deps: 
ExploreRouteDeps): void {
+  const auth = requireAuth(deps);
+
+  app.post(
+    '/api/explore/query',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const body = (req.body ?? {}) as ExploreRequest;
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+      const offset = await getServerOffsetMinutes(deps.config, deps.fetch);
+      const generatedAt = Date.now();
+
+      if (body.kind === 'trace') {
+        const traceSource = body.traceSource ?? 'native';
+        const maxTraces = 
deps.config.current.performance.limits.maxPageSize.traces;
+        const win = traceWindowFields(body.window ?? {});
+        const base: TraceListBody = {
+          ...win,
+          traceId: body.traceId,
+          traceState: body.traceState,
+          queryOrder: body.queryOrder,
+          minTraceDuration: body.minTraceDuration,
+          maxTraceDuration: body.maxTraceDuration,
+          tags: body.tags,
+          pageNum: body.pageNum,
+          pageSize: body.pageSize,
+        };
+
+        if (traceSource === 'native') {
+          const ids = resolveNativeEntity(body.entity);
+          if (!ids.serviceId && !body.traceId) {
+            return reply.code(400).send({ error: 'entity_required' });
+          }
+          const native = await fetchNativeList(
+            opts,
+            { ...base, ...ids },
+            '', // layer-less: serviceId is pre-resolved, resolveServiceId 
never runs
+            !!req.coldStage,
+            offset,
+            maxTraces,
+          );
+          const resolved: ExploreResolved = {
+            kind: 'trace',
+            source: 'native',
+            backend: native.api,
+            entityId: ids.serviceId,
+            condition: {
+              ...ids,
+              ...(body.traceId ? { traceId: body.traceId } : {}),
+              traceState: body.traceState ?? 'ALL',
+              queryOrder: body.queryOrder ?? 'BY_START_TIME',
+              ...win,
+            },
+          };
+          return reply.send({
+            kind: 'trace',
+            traceSource: 'native',
+            generatedAt,
+            native,
+            resolved,
+          } satisfies ExploreResponse);
+        }
+
+        // zipkin: a raw service name (no OAP id). remoteService/span/
+        // annotation enrichment lands with the zipkin form increment.
+        const service = body.entity.serviceName;
+        const zipkin = await fetchZipkinList(opts, { ...base, service }, 
maxTraces);
+        const resolved: ExploreResolved = {
+          kind: 'trace',
+          source: 'zipkin',
+          entityId: service,
+          condition: {
+            ...(service ? { serviceName: service } : {}),
+            ...(typeof body.minTraceDuration === 'number' ? { minDuration: 
body.minTraceDuration } : {}),
+            ...(typeof body.maxTraceDuration === 'number' ? { maxDuration: 
body.maxTraceDuration } : {}),
+            ...win,
+          },
+        };
+        return reply.send({
+          kind: 'trace',
+          traceSource: 'zipkin',
+          generatedAt,
+          zipkin,
+          resolved,
+        } satisfies ExploreResponse);
+      }
+
+      // kind === 'log' — raw + browser, added in the log increment.
+      return reply.code(501).send({ error: 'log_not_implemented' });
+    },
+  );
+}
diff --git a/apps/bff/src/http/query/trace.ts b/apps/bff/src/http/query/trace.ts
index 896cbb5..c615745 100644
--- a/apps/bff/src/http/query/trace.ts
+++ b/apps/bff/src/http/query/trace.ts
@@ -107,7 +107,7 @@ function explicitWindow(
 
 // ── Wire request shape ─────────────────────────────────────────────
 
-interface TraceListBody {
+export interface TraceListBody {
   source?: TraceSource;
   service?: string;
   serviceId?: string;
@@ -295,7 +295,7 @@ function buildTraceCondition(
   };
 }
 
-async function fetchNativeList(
+export async function fetchNativeList(
   opts: GraphqlOptions,
   body: TraceListBody,
   layerKey: string,
@@ -382,7 +382,7 @@ async function fetchNativeList(
   }
 }
 
-async function fetchZipkinList(
+export async function fetchZipkinList(
   opts: GraphqlOptions,
   body: TraceListBody,
   maxPageSize: number,
diff --git a/apps/bff/src/rbac/route-policy.ts 
b/apps/bff/src/rbac/route-policy.ts
index 87c8151..0b81b4e 100644
--- a/apps/bff/src/rbac/route-policy.ts
+++ b/apps/bff/src/rbac/route-policy.ts
@@ -96,6 +96,8 @@ export const ROUTE_POLICY: Record<string, RoutePolicy> = {
   'GET /api/browser-errors/source-maps':           'browser-errors:read',
   'POST /api/browser-errors/resolve':              'browser-errors:read',
 
+  'POST /api/explore/query':                       'explore:read',
+
   'GET /api/layer/:key/topology':                  'topology:read',
   'GET /api/layer/:key/instance-topology':         'topology:read',
   'GET /api/layer/:key/deployment':                'topology:read',
diff --git a/apps/bff/src/server.ts b/apps/bff/src/server.ts
index 55da584..03be503 100644
--- a/apps/bff/src/server.ts
+++ b/apps/bff/src/server.ts
@@ -45,6 +45,7 @@ import { registerTraceTagRoutes } from 
'./http/query/trace-tag.js';
 import { registerZipkinRoutes } from './http/query/zipkin.js';
 import { registerLogRoute } from './http/query/log.js';
 import { registerBrowserErrorsRoute } from './http/query/browser-errors.js';
+import { registerExploreRoutes } from './http/query/explore.js';
 import { registerPodLogRoutes } from './http/query/pod-log.js';
 import { registerDashboardQueryRoute } from './http/query/dashboard.js';
 import { registerAlarmsQueryRoutes } from './http/query/alarms.js';
@@ -229,6 +230,7 @@ registerTraceTagRoutes(app, { config: source, sessions });
 registerZipkinRoutes(app, { config: source, sessions });
 registerLogRoute(app, { config: source, sessions });
 registerBrowserErrorsRoute(app, { config: source, sessions });
+registerExploreRoutes(app, { config: source, sessions });
 registerPodLogRoutes(app, { config: source, sessions });
 registerDashboardQueryRoute(app, {
   config: source,
diff --git a/apps/bff/src/util/entityId.ts b/apps/bff/src/util/entityId.ts
new file mode 100644
index 0000000..0f0158c
--- /dev/null
+++ b/apps/bff/src/util/entityId.ts
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Build OAP entity ids from names — the inverse of the UI's oalEntityId
+ * decoder. Explore's Type/manual mode uses this so a query needs no layer
+ * and no `listServices` round-trip: the operator types a service name +
+ * the real/normal flag and we encode the id OAP would have minted.
+ *
+ * Mirrors OAP `IDManager` (verified against skywalking IDManager.java):
+ *   service  = base64(name) + "." + (isReal ? "1" : "0")
+ *   instance = serviceId   + "_" + base64(instanceName)
+ *   endpoint = serviceId   + "_" + base64(endpointName)
+ * Standard padded base64 over UTF-8 bytes (OAP's `Base64.getEncoder()`,
+ * NOT url-safe — the id regex accepts `+/=`). A blank name encodes the
+ * `_blank` sentinel, matching `Const.BLANK_ENTITY_NAME`.
+ */
+
+const BLANK_ENTITY_NAME = '_blank';
+
+function encode(name: string): string {
+  const n = name.length === 0 ? BLANK_ENTITY_NAME : name;
+  return Buffer.from(n, 'utf8').toString('base64');
+}
+
+export function buildServiceId(serviceName: string, isReal: boolean): string {
+  return `${encode(serviceName)}.${isReal ? '1' : '0'}`;
+}
+
+export function buildInstanceId(serviceId: string, instanceName: string): 
string {
+  return `${serviceId}_${encode(instanceName)}`;
+}
+
+export function buildEndpointId(serviceId: string, endpointName: string): 
string {
+  return `${serviceId}_${encode(endpointName)}`;
+}
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index abcb114..995b4ac 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -88,6 +88,7 @@ import { PprofApi } from './scopes/pprof';
 import { DslApi } from './scopes/dsl';
 import { LiveDebugApi } from './scopes/live-debug';
 import { InspectApi } from './scopes/inspect';
+import { ExploreApi } from './scopes/explore';
 import { OapOpsApi } from './scopes/oap-ops';
 import { AlarmsApi } from './scopes/alarms';
 import { Infra3dApi } from './scopes/infra-3d';
@@ -245,6 +246,14 @@ export type {
   PprofProgressResponse,
   PprofAnalyzeResponse,
   PprofTaskCreationResponse,
+  ExploreKind,
+  ExploreTraceSource,
+  ExploreLogSource,
+  ExploreEntity,
+  ExploreWindow,
+  ExploreRequest,
+  ExploreResolved,
+  ExploreResponse,
 } from '@skywalking-horizon-ui/api-client';
 
 /** Query shape for `/api/zipkin/traces`. Mirrors the Zipkin v2 REST
@@ -902,6 +911,7 @@ export class BffClient {
   readonly dsl = new DslApi(this);
   readonly liveDebug = new LiveDebugApi(this);
   readonly inspect = new InspectApi(this);
+  readonly explore = new ExploreApi(this);
   readonly oapOps = new OapOpsApi(this);
   readonly alarms = new AlarmsApi(this);
   readonly infra3d = new Infra3dApi(this);
diff --git a/apps/ui/src/api/scopes/explore.ts 
b/apps/ui/src/api/scopes/explore.ts
new file mode 100644
index 0000000..9124266
--- /dev/null
+++ b/apps/ui/src/api/scopes/explore.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { ExploreRequest, ExploreResponse } from 
'@skywalking-horizon-ui/api-client';
+import type { BffClient } from '../client';
+
+/** `bff.explore` — cross-layer trace/log query (one POST → one result). */
+export class ExploreApi {
+  constructor(private readonly bff: BffClient) {}
+
+  query(req: ExploreRequest): Promise<ExploreResponse> {
+    return this.bff.request<ExploreResponse>('POST', '/api/explore/query', 
req);
+  }
+}
diff --git a/apps/ui/src/features/operate/explore/ExploreView.vue 
b/apps/ui/src/features/operate/explore/ExploreView.vue
new file mode 100644
index 0000000..e1ee616
--- /dev/null
+++ b/apps/ui/src/features/operate/explore/ExploreView.vue
@@ -0,0 +1,535 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!--
+  Explore — cross-layer trace/log query power-tool.
+
+  Layer-less by design: the operator names an entity by PICKING it (a
+  layer-filtered dropdown) or TYPING it (service name + the real/normal
+  flag, which the BFF encodes into the OAP id). One query → one result,
+  rendered with the same waterfall the per-layer Traces tab uses. The
+  resolved-query panel surfaces the exact condition the BFF ran — the
+  trace/log analog of Metrics-inspect showing the bare MQE.
+
+  This pass implements Trace · native end to end. Trace · zipkin and the
+  Log kinds (raw / browser-error) are wired into the same spine next.
+-->
+<script setup lang="ts">
+import { computed, reactive, ref, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { bffClient } from '@/api/client';
+import type {
+  ExploreEntity,
+  ExploreRequest,
+  ExploreResolved,
+  LayerDef,
+  NativeSpan,
+  NativeTraceListResponse,
+  NativeTraceListRow,
+  TraceQueryOrder,
+  TraceQueryState,
+} from '@/api/client';
+import { useLayers } from '@/shell/useLayers';
+import { useTraceDetail } from '@/layer/traces/useLayerTraces';
+import NativeTraceWaterfall, { type WaterfallSpan } from 
'@/layer/traces/NativeTraceWaterfall.vue';
+import TypeaheadSelect from '@/components/primitives/TypeaheadSelect.vue';
+
+const { t } = useI18n();
+const { layers } = useLayers();
+
+// ── kind + source (zipkin / log land in the next increments) ──────────
+type Kind = 'trace' | 'log';
+type TraceSource = 'native' | 'zipkin';
+const kind = ref<Kind>('trace');
+const traceSource = ref<TraceSource>('native');
+
+// ── entity: pick (layer-filtered) vs type (name + real flag) ──────────
+type EntityMode = 'pick' | 'type';
+const entityMode = ref<EntityMode>('pick');
+
+// Layers that actually serve native traces.
+const traceLayers = computed<LayerDef[]>(() =>
+  layers.value.filter((l) => l.caps?.traces && (l.traces?.source ?? 'native') 
!== 'zipkin'),
+);
+
+// pick mode
+const pickLayer = ref<string>('');
+const pickServiceId = ref<string>('');
+const pickInstanceId = ref<string>('');
+const pickEndpointId = ref<string>('');
+const services = ref<Array<{ id: string; name: string; normal: boolean | null 
}>>([]);
+const instances = ref<Array<{ id: string; name: string }>>([]);
+const endpoints = ref<Array<{ id: string; name: string }>>([]);
+const endpointQuery = ref<string>('');
+const servicesLoading = ref(false);
+
+const pickServiceName = computed(
+  () => services.value.find((s) => s.id === pickServiceId.value)?.name ?? '',
+);
+
+async function loadServices(): Promise<void> {
+  services.value = [];
+  instances.value = [];
+  endpoints.value = [];
+  pickServiceId.value = '';
+  pickInstanceId.value = '';
+  pickEndpointId.value = '';
+  if (!pickLayer.value) return;
+  servicesLoading.value = true;
+  try {
+    const res = await bffClient.layer.services(pickLayer.value);
+    services.value = res.reachable ? res.services : [];
+  } catch {
+    services.value = [];
+  } finally {
+    servicesLoading.value = false;
+  }
+}
+
+async function loadInstances(): Promise<void> {
+  instances.value = [];
+  pickInstanceId.value = '';
+  const name = pickServiceName.value;
+  if (!pickLayer.value || !name) return;
+  try {
+    const res = await bffClient.layer.instances(pickLayer.value, name);
+    instances.value = res.reachable ? res.instances : [];
+  } catch {
+    instances.value = [];
+  }
+}
+
+async function loadEndpoints(): Promise<void> {
+  const name = pickServiceName.value;
+  if (!pickLayer.value || !name) {
+    endpoints.value = [];
+    return;
+  }
+  try {
+    const res = await bffClient.layer.endpoints(pickLayer.value, name, 
endpointQuery.value.trim());
+    endpoints.value = res.reachable ? res.endpoints : [];
+  } catch {
+    endpoints.value = [];
+  }
+}
+
+watch(pickLayer, () => void loadServices());
+watch(pickServiceId, () => {
+  void loadInstances();
+  void loadEndpoints();
+});
+watch(endpointQuery, () => void loadEndpoints());
+
+const serviceOptions = computed(() =>
+  services.value.map((s) => ({ value: s.id, label: s.name, hint: s.normal === 
false ? 'virtual' : undefined })),
+);
+const instanceOptions = computed(() => [
+  { value: '', label: t('All instances') },
+  ...instances.value.map((i) => ({ value: i.id, label: i.name })),
+]);
+const endpointOptions = computed(() => [
+  { value: '', label: t('All endpoints') },
+  ...endpoints.value.map((e) => ({ value: e.id, label: e.name })),
+]);
+const instanceSel = computed<string>({ get: () => pickInstanceId.value, set: 
(v) => (pickInstanceId.value = v ?? '') });
+const endpointSel = computed<string>({ get: () => pickEndpointId.value, set: 
(v) => (pickEndpointId.value = v ?? '') });
+
+// type mode
+const typeService = ref<string>('');
+const typeReal = ref<boolean>(true);
+const typeInstance = ref<string>('');
+const typeEndpoint = ref<string>('');
+
+/** Seed the Type form from the current Pick selection — pick to discover,
+ *  then tweak the name/flag by hand. */
+function seedTypeFromPick(): void {
+  if (!pickServiceName.value) return;
+  typeService.value = pickServiceName.value;
+  typeReal.value = services.value.find((s) => s.id === 
pickServiceId.value)?.normal !== false;
+  typeInstance.value = instances.value.find((i) => i.id === 
pickInstanceId.value)?.name ?? '';
+  typeEndpoint.value = endpoints.value.find((e) => e.id === 
pickEndpointId.value)?.name ?? '';
+  entityMode.value = 'type';
+}
+
+function currentEntity(): ExploreEntity | null {
+  if (entityMode.value === 'pick') {
+    if (!pickServiceId.value) return null;
+    return {
+      mode: 'pick',
+      serviceId: pickServiceId.value,
+      instanceId: pickInstanceId.value || undefined,
+      endpointId: pickEndpointId.value || undefined,
+    };
+  }
+  const name = typeService.value.trim();
+  if (!name) return null;
+  return {
+    mode: 'type',
+    serviceName: name,
+    isReal: typeReal.value,
+    instanceName: typeInstance.value.trim() || undefined,
+    endpointName: typeEndpoint.value.trim() || undefined,
+  };
+}
+
+// ── trace conditions ──────────────────────────────────────────────────
+const cond = reactive({
+  traceId: '',
+  traceState: 'ALL' as TraceQueryState,
+  queryOrder: 'BY_START_TIME' as TraceQueryOrder,
+  minDuration: '' as string,
+  maxDuration: '' as string,
+  tags: '' as string,
+  windowMinutes: 30,
+  limit: 30,
+});
+const WINDOWS = [15, 30, 60, 180, 360, 720, 1440];
+const LIMITS = [20, 30, 50, 100];
+
+function parseTags(s: string): Array<{ key: string; value: string }> {
+  return s
+    .split(',')
+    .map((p) => p.trim())
+    .filter(Boolean)
+    .map((p) => {
+      const i = p.indexOf('=');
+      return i < 0 ? { key: p, value: '' } : { key: p.slice(0, i).trim(), 
value: p.slice(i + 1).trim() };
+    })
+    .filter((kv) => kv.key);
+}
+
+// ── run + result ──────────────────────────────────────────────────────
+const running = ref(false);
+const hasQueried = ref(false);
+const errorMsg = ref<string | null>(null);
+const native = ref<NativeTraceListResponse | null>(null);
+const resolved = ref<ExploreResolved | null>(null);
+const showResolved = ref(false);
+
+const canRun = computed(() => kind.value === 'trace' && traceSource.value === 
'native' && currentEntity() !== null);
+
+async function runQuery(): Promise<void> {
+  const entity = currentEntity();
+  if (!entity) return;
+  running.value = true;
+  hasQueried.value = true;
+  errorMsg.value = null;
+  selectedTraceId.value = null;
+  embeddedSpans.value = null;
+  const req: ExploreRequest = {
+    kind: 'trace',
+    traceSource: 'native',
+    entity,
+    window: { windowMinutes: cond.windowMinutes },
+    pageSize: cond.limit,
+    traceId: cond.traceId.trim() || undefined,
+    traceState: cond.traceState,
+    queryOrder: cond.queryOrder,
+    minTraceDuration: cond.minDuration ? Number(cond.minDuration) : undefined,
+    maxTraceDuration: cond.maxDuration ? Number(cond.maxDuration) : undefined,
+    tags: parseTags(cond.tags),
+  };
+  try {
+    const res = await bffClient.explore.query(req);
+    if (res.kind === 'trace' && res.traceSource === 'native') {
+      native.value = res.native;
+      resolved.value = res.resolved;
+      if (!res.native.reachable) errorMsg.value = res.native.error ?? t('OAP 
unreachable');
+    }
+  } catch (e) {
+    errorMsg.value = e instanceof Error ? e.message : String(e);
+    native.value = null;
+  } finally {
+    running.value = false;
+  }
+}
+
+const rows = computed<NativeTraceListRow[]>(() => (hasQueried.value ? 
native.value?.traces ?? [] : []));
+
+// ── detail (reuses the native waterfall + GET /api/trace/:id) ─────────
+const selectedTraceId = ref<string | null>(null);
+const embeddedSpans = ref<NativeSpan[] | null>(null);
+const selectedSpan = ref<WaterfallSpan | null>(null);
+const sourceRef = ref<'native' | 'zipkin'>('native');
+const { nativeDetail, isFetching: detailFetching } = 
useTraceDetail(selectedTraceId, sourceRef);
+const waterfallSpans = computed<NativeSpan[]>(() => embeddedSpans.value ?? 
nativeDetail.value?.spans ?? []);
+
+function openRow(row: NativeTraceListRow): void {
+  selectedSpan.value = null;
+  selectedTraceId.value = row.traceIds[0] ?? null;
+  embeddedSpans.value = row.spans ?? null;
+}
+
+function fmtDur(ms: number): string {
+  return ms >= 1000 ? `${(ms / 1000).toFixed(2)} s` : `${ms} ms`;
+}
+function fmtTime(start: string): string {
+  const n = Number(start);
+  return Number.isFinite(n) && n > 0 ? new Date(n).toLocaleTimeString() : 
start;
+}
+</script>
+
+<template>
+  <div class="ex">
+    <header class="ex-head">
+      <h1>{{ t('Explore') }}</h1>
+      <p class="ex-sub">{{ t('Cross-layer trace and log query. Pick any 
service, or type its name.') }}</p>
+    </header>
+
+    <div class="ex-bar">
+      <div class="ex-seg">
+        <button :class="{ on: kind === 'trace' }" @click="kind = 'trace'">{{ 
t('Trace') }}</button>
+        <button class="muted" disabled :title="t('coming next')">{{ t('Log') 
}}</button>
+      </div>
+      <div class="ex-seg" v-if="kind === 'trace'">
+        <button :class="{ on: traceSource === 'native' }" @click="traceSource 
= 'native'">{{ t('Native') }}</button>
+        <button class="muted" disabled :title="t('coming 
next')">Zipkin</button>
+      </div>
+    </div>
+
+    <div class="ex-form">
+      <div class="ex-seg sm">
+        <button :class="{ on: entityMode === 'pick' }" @click="entityMode = 
'pick'">{{ t('Pick') }}</button>
+        <button :class="{ on: entityMode === 'type' }" @click="entityMode = 
'type'">{{ t('Type') }}</button>
+      </div>
+
+      <!-- Pick: layer-filtered dropdowns -->
+      <template v-if="entityMode === 'pick'">
+        <label class="ex-f">
+          <span>{{ t('Layer') }}</span>
+          <TypeaheadSelect
+            v-model="pickLayer"
+            :aria-label="t('Layer')"
+            :options="traceLayers.map((l) => ({ value: l.key, label: l.name || 
l.key }))"
+            :placeholder="t('Select a layer')"
+          />
+        </label>
+        <label class="ex-f">
+          <span>{{ t('Service') }}</span>
+          <TypeaheadSelect
+            v-model="pickServiceId"
+            :aria-label="t('Service')"
+            :options="serviceOptions"
+            :disabled="!pickLayer || servicesLoading"
+            :placeholder="servicesLoading ? t('Reading…') : t('Select a 
service')"
+          />
+        </label>
+        <label class="ex-f">
+          <span>{{ t('Instance') }}</span>
+          <TypeaheadSelect
+            v-model="instanceSel"
+            :aria-label="t('Instance')"
+            :options="instanceOptions"
+            :disabled="!pickServiceId"
+            :placeholder="t('All instances')"
+          />
+        </label>
+        <label class="ex-f cf-wide">
+          <span>{{ t('Endpoint') }}</span>
+          <input
+            v-model="endpointQuery"
+            class="ex-input"
+            type="text"
+            :disabled="!pickServiceId"
+            :placeholder="t('Search endpoints…')"
+          />
+          <TypeaheadSelect
+            v-model="endpointSel"
+            :aria-label="t('Endpoint')"
+            :options="endpointOptions"
+            :disabled="!pickServiceId"
+            :placeholder="t('All endpoints')"
+          />
+        </label>
+        <button class="ex-link" :disabled="!pickServiceId" 
@click="seedTypeFromPick">{{ t('→ edit as text') }}</button>
+      </template>
+
+      <!-- Type: name + real flag (no layer) -->
+      <template v-else>
+        <label class="ex-f">
+          <span>{{ t('Service name') }}</span>
+          <input v-model="typeService" class="ex-input" type="text" 
:placeholder="t('e.g. agent::checkout')" />
+        </label>
+        <label class="ex-f chk">
+          <input v-model="typeReal" type="checkbox" />
+          <span>{{ t('real') }}</span>
+          <small class="dim">{{ t('agent-reported (off = virtual/peer)') 
}}</small>
+        </label>
+        <label class="ex-f">
+          <span>{{ t('Instance') }}</span>
+          <input v-model="typeInstance" class="ex-input" type="text" 
:placeholder="t('optional')" />
+        </label>
+        <label class="ex-f">
+          <span>{{ t('Endpoint') }}</span>
+          <input v-model="typeEndpoint" class="ex-input" type="text" 
:placeholder="t('optional')" />
+        </label>
+      </template>
+    </div>
+
+    <div class="ex-form">
+      <label class="ex-f">
+        <span>{{ t('Trace ID') }}</span>
+        <input v-model="cond.traceId" class="ex-input mono" type="text" 
:placeholder="t('paste a trace id')" />
+      </label>
+      <label class="ex-f">
+        <span>{{ t('Status') }}</span>
+        <select v-model="cond.traceState" class="ex-input">
+          <option value="ALL">{{ t('All') }}</option>
+          <option value="SUCCESS">{{ t('Success') }}</option>
+          <option value="ERROR">{{ t('Error') }}</option>
+        </select>
+      </label>
+      <label class="ex-f">
+        <span>{{ t('Order') }}</span>
+        <select v-model="cond.queryOrder" class="ex-input">
+          <option value="BY_START_TIME">{{ t('Newest') }}</option>
+          <option value="BY_DURATION">{{ t('Slowest') }}</option>
+        </select>
+      </label>
+      <label class="ex-f sm">
+        <span>{{ t('Duration (ms)') }}</span>
+        <span class="ex-dur">
+          <input v-model="cond.minDuration" class="ex-input" type="number" 
min="0" :placeholder="t('min')" />
+          <input v-model="cond.maxDuration" class="ex-input" type="number" 
min="0" :placeholder="t('max')" />
+        </span>
+      </label>
+      <label class="ex-f cf-wide">
+        <span>{{ t('Tags') }}</span>
+        <input v-model="cond.tags" class="ex-input mono" type="text" 
:placeholder="t('http.status_code=500, …')" />
+      </label>
+      <label class="ex-f">
+        <span>{{ t('Time') }}</span>
+        <select v-model.number="cond.windowMinutes" class="ex-input">
+          <option v-for="w in WINDOWS" :key="w" :value="w">{{ w < 60 ? `${w}m` 
: `${w / 60}h` }}</option>
+        </select>
+      </label>
+      <label class="ex-f">
+        <span>{{ t('Limit') }}</span>
+        <select v-model.number="cond.limit" class="ex-input">
+          <option v-for="l in LIMITS" :key="l" :value="l">{{ l }}</option>
+        </select>
+      </label>
+      <button class="ex-run" :disabled="!canRun || running" @click="runQuery">
+        {{ running ? t('Running…') : t('Run query') }}
+      </button>
+    </div>
+
+    <div v-if="resolved" class="ex-resolved">
+      <button class="ex-resolved-tog" @click="showResolved = !showResolved">
+        {{ showResolved ? '▾' : '▸' }} {{ t('Resolved query') }}
+        <span class="dim">{{ resolved.source }}{{ resolved.backend ? ` · 
${resolved.backend}` : '' }}</span>
+      </button>
+      <pre v-if="showResolved" class="ex-resolved-body">{{ 
JSON.stringify(resolved.condition, null, 2) }}</pre>
+    </div>
+
+    <div class="ex-result">
+      <div v-if="!hasQueried" class="ex-empty">{{ t('Pick or type an entity, 
then Run query.') }}</div>
+      <div v-else-if="errorMsg" class="ex-err">{{ errorMsg }}</div>
+      <div v-else-if="rows.length === 0" class="ex-empty">{{ t('No traces in 
this window.') }}</div>
+      <div v-else class="ex-split">
+        <ul class="ex-list">
+          <li
+            v-for="(row, i) in rows"
+            :key="row.key || i"
+            class="ex-row"
+            :class="{ on: selectedTraceId === (row.traceIds[0] ?? ''), err: 
row.isError }"
+            @click="openRow(row)"
+          >
+            <span class="ex-flag" :class="{ err: row.isError }"></span>
+            <span class="ex-ep">{{ row.endpointNames[0] || row.traceIds[0] || 
row.key }}</span>
+            <span class="ex-dur-v">{{ fmtDur(row.duration) }}</span>
+            <span class="ex-time">{{ fmtTime(row.start) }}</span>
+          </li>
+        </ul>
+        <div class="ex-detail">
+          <div v-if="!selectedTraceId" class="ex-empty sm">{{ t('Select a 
trace.') }}</div>
+          <div v-else-if="detailFetching && waterfallSpans.length === 0" 
class="ex-empty sm">{{ t('Reading trace…') }}</div>
+          <div v-else-if="waterfallSpans.length === 0" class="ex-empty sm">{{ 
t('No spans (older than the detail window).') }}</div>
+          <NativeTraceWaterfall
+            v-else
+            :spans="waterfallSpans"
+            :selected-span="selectedSpan"
+            @select-span="selectedSpan = $event"
+          />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.ex { display: flex; flex-direction: column; gap: 10px; height: 100%; 
min-height: 0; padding: 14px 16px; }
+.ex-head h1 { font-size: 16px; margin: 0; }
+.ex-sub { color: var(--sw-fg-3); font-size: 12px; margin: 2px 0 0; }
+.ex-bar { display: flex; gap: 12px; align-items: center; }
+.ex-seg { display: inline-flex; border: 1px solid var(--sw-line-2); 
border-radius: 5px; overflow: hidden; }
+.ex-seg.sm button { padding: 3px 10px; }
+.ex-seg button {
+  background: var(--sw-bg-2); color: var(--sw-fg-2); border: none; padding: 
4px 14px;
+  font: inherit; font-size: 12px; cursor: pointer;
+}
+.ex-seg button + button { border-left: 1px solid var(--sw-line-2); }
+.ex-seg button.on { background: var(--sw-accent); color: #fff; }
+.ex-seg button.muted { opacity: 0.45; cursor: not-allowed; }
+.ex-form { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: 
flex-end; }
+.ex-f { display: flex; flex-direction: column; gap: 3px; font-size: 11px; 
color: var(--sw-fg-3); }
+.ex-f.cf-wide { min-width: 240px; flex: 1 1 240px; }
+.ex-f.chk { flex-direction: row; align-items: center; gap: 6px; 
padding-bottom: 4px; }
+.ex-f.chk .dim { color: var(--sw-fg-4); }
+.ex-input {
+  background: var(--sw-bg-2); color: var(--sw-fg-1); border: 1px solid 
var(--sw-line-2);
+  border-radius: 4px; padding: 3px 8px; font: inherit; font-size: 12px; 
min-width: 140px;
+}
+.ex-input.mono { font-family: var(--sw-mono); }
+.ex-dur { display: inline-flex; gap: 6px; }
+.ex-dur .ex-input { min-width: 70px; width: 80px; }
+.ex-link { background: none; border: none; color: var(--sw-accent); font-size: 
11px; cursor: pointer; padding: 0 0 4px; }
+.ex-link:disabled { color: var(--sw-fg-4); cursor: not-allowed; }
+.ex-run {
+  background: var(--sw-accent); color: #fff; border: none; border-radius: 4px;
+  padding: 5px 16px; font: inherit; font-size: 12px; cursor: pointer; height: 
28px;
+}
+.ex-run:disabled { opacity: 0.5; cursor: not-allowed; }
+.ex-resolved { border: 1px solid var(--sw-line); border-radius: 5px; }
+.ex-resolved-tog {
+  width: 100%; text-align: left; background: var(--sw-bg-1); color: 
var(--sw-fg-2);
+  border: none; padding: 5px 10px; font: inherit; font-size: 11px; cursor: 
pointer;
+}
+.ex-resolved-tog .dim { color: var(--sw-fg-4); margin-left: 8px; }
+.ex-resolved-body {
+  margin: 0; padding: 8px 12px; font-family: var(--sw-mono); font-size: 11px;
+  color: var(--sw-fg-2); background: var(--sw-bg-0); overflow: auto; 
max-height: 180px;
+  border-top: 1px solid var(--sw-line);
+}
+.ex-result { flex: 1; min-height: 0; border: 1px solid var(--sw-line); 
border-radius: 5px; overflow: hidden; }
+.ex-empty, .ex-err { padding: 24px; text-align: center; color: var(--sw-fg-3); 
font-size: 12px; }
+.ex-empty.sm { padding: 14px; }
+.ex-err { color: var(--sw-err); }
+.ex-split { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; 
height: 100%; min-height: 0; }
+.ex-list { list-style: none; margin: 0; padding: 0; overflow: auto; 
border-right: 1px solid var(--sw-line); }
+.ex-row {
+  display: grid; grid-template-columns: 10px 1fr auto auto; gap: 8px; 
align-items: center;
+  padding: 5px 10px; cursor: pointer; font-size: 12px; border-bottom: 1px 
solid var(--sw-line);
+}
+.ex-row:hover { background: var(--sw-bg-2); }
+.ex-row.on { background: var(--sw-bg-3); }
+.ex-flag { width: 8px; height: 8px; border-radius: 50%; background: 
var(--sw-ok, #3a7); }
+.ex-flag.err { background: var(--sw-err); }
+.ex-ep { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.ex-dur-v { font-family: var(--sw-mono); font-size: 11px; color: 
var(--sw-fg-2); }
+.ex-time { font-size: 11px; color: var(--sw-fg-3); }
+.ex-detail { overflow: auto; min-width: 0; }
+</style>
diff --git a/apps/ui/src/shell/AppSidebar.vue b/apps/ui/src/shell/AppSidebar.vue
index 83422fb..69c574a 100644
--- a/apps/ui/src/shell/AppSidebar.vue
+++ b/apps/ui/src/shell/AppSidebar.vue
@@ -272,6 +272,7 @@ const sections = computed<NavSection[]>(() => [
       },
       { icon: 'event', label: t('Capture history'), to: 
'/operate/live-debug/history', verb: 'live-debug:read' },
       { icon: 'metric', label: t('Metrics inspect'), to: '/operate/inspect', 
verb: 'inspect:read' },
+      { icon: 'trace', label: t('Explore'), to: '/operate/explore', verb: 
'explore:read' },
     ],
   },
   {
diff --git a/apps/ui/src/shell/router/index.ts 
b/apps/ui/src/shell/router/index.ts
index 98c5504..ef9c763 100644
--- a/apps/ui/src/shell/router/index.ts
+++ b/apps/ui/src/shell/router/index.ts
@@ -211,6 +211,16 @@ const shellRoutes: RouteRecordRaw[] = [
     component: () => import('@/features/operate/inspect/InspectView.vue'),
     meta: { verb: 'inspect:read' },
   },
+  // Explore — cross-layer trace/log power-tool. Layer-less: the entity is
+  // picked or typed (name + real flag); dispatches by source via
+  // /api/explore/query. Uses the standard query-protocol (always on),
+  // so it is NOT gated on the inspect module like Metrics inspect.
+  {
+    path: 'operate/explore',
+    name: 'explore',
+    component: () => import('@/features/operate/explore/ExploreView.vue'),
+    meta: { verb: 'explore:read' },
+  },
   // Data retention (TTL) — query-port read of getRecordsTTL/getMetricsTTL.
   {
     path: 'operate/ttl',
diff --git a/packages/api-client/src/explore.ts 
b/packages/api-client/src/explore.ts
new file mode 100644
index 0000000..39061b9
--- /dev/null
+++ b/packages/api-client/src/explore.ts
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Explore — cross-layer trace/log query power-tool wire contract.
+ *
+ * One POST /api/explore/query carries a `kind` (trace|log) + a source
+ * (native|zipkin for trace, raw|browser for log) + an entity + a window,
+ * and the BFF dispatches onto the SAME query logic the per-layer Traces/
+ * Logs tabs use — but without a layer. The entity is identified by a
+ * pre-resolved OAP id (Pick mode) or a name + `real` flag (Type mode);
+ * the BFF encodes `base64(name).{1|0}` so no `listServices(layer)` is
+ * needed. Responses reuse the per-layer response shapes verbatim, tagged
+ * by kind+source so the UI renderer narrows.
+ */
+
+import type {
+  NativeTraceListResponse,
+  TraceQueryOrder,
+  TraceQueryState,
+  ZipkinTraceListResponse,
+} from './trace.js';
+import type { LogsResponse } from './logs.js';
+import type { BrowserErrorCategory, BrowserErrorsResponse } from 
'./browser-errors.js';
+
+export type ExploreKind = 'trace' | 'log';
+export type ExploreTraceSource = 'native' | 'zipkin';
+export type ExploreLogSource = 'raw' | 'browser';
+
+/**
+ * How the operator named the entity. Pick mode forwards OAP ids the
+ * metadata routes already minted; Type mode supplies a name + the
+ * real/normal flag and the BFF builds the id (`base64(name).{1|0}`),
+ * so a layer is never needed. Zipkin uses a raw service name (no id).
+ */
+export interface ExploreEntity {
+  mode: 'pick' | 'type';
+  /** Pick: pre-resolved OAP service id (native/raw/browser). */
+  serviceId?: string;
+  instanceId?: string;
+  endpointId?: string;
+  /** Type: name + flag the BFF encodes. Also the zipkin service name. */
+  serviceName?: string;
+  isReal?: boolean;
+  instanceName?: string;
+  endpointName?: string;
+}
+
+/** Self-owned triage window (SECOND precision). Explicit start/end (epoch
+ *  ms) override the rolling `windowMinutes`. */
+export interface ExploreWindow {
+  windowMinutes?: number;
+  startMs?: number;
+  endMs?: number;
+}
+
+export interface ExploreRequest {
+  kind: ExploreKind;
+  /** when kind === 'trace' */
+  traceSource?: ExploreTraceSource;
+  /** when kind === 'log' */
+  logSource?: ExploreLogSource;
+  entity: ExploreEntity;
+  window: ExploreWindow;
+  pageNum?: number;
+  pageSize?: number;
+  // ── trace conditions ──
+  traceId?: string;
+  traceState?: TraceQueryState;
+  queryOrder?: TraceQueryOrder;
+  minTraceDuration?: number;
+  maxTraceDuration?: number;
+  tags?: Array<{ key: string; value: string }>;
+  // ── zipkin-only (service name lives on entity.serviceName) ──
+  remoteServiceName?: string;
+  spanName?: string;
+  annotationQuery?: string;
+  // ── log (raw) conditions ──
+  keywordsOfContent?: string[];
+  relatedTraceId?: string;
+  // ── log (browser) condition ──
+  category?: BrowserErrorCategory;
+}
+
+/** Query-transparency echo: what the BFF actually resolved + ran. */
+export interface ExploreResolved {
+  kind: ExploreKind;
+  source: ExploreTraceSource | ExploreLogSource;
+  /** native: 'queryTraces' | 'queryBasicTraces'. */
+  backend?: string;
+  /** Resolved OAP id (native/raw/browser) or zipkin service name. */
+  entityId?: string;
+  /** The condition the BFF sent (display-only). */
+  condition: Record<string, unknown>;
+}
+
+export type ExploreResponse =
+  | { kind: 'trace'; traceSource: 'native'; generatedAt: number; native: 
NativeTraceListResponse; resolved: ExploreResolved }
+  | { kind: 'trace'; traceSource: 'zipkin'; generatedAt: number; zipkin: 
ZipkinTraceListResponse; resolved: ExploreResolved }
+  | { kind: 'log'; logSource: 'raw'; generatedAt: number; logs: LogsResponse; 
resolved: ExploreResolved }
+  | { kind: 'log'; logSource: 'browser'; generatedAt: number; browser: 
BrowserErrorsResponse; resolved: ExploreResolved };
diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts
index 100e338..36898fa 100644
--- a/packages/api-client/src/index.ts
+++ b/packages/api-client/src/index.ts
@@ -293,6 +293,16 @@ export {
   type MqeOwner,
   type InspectExecRequest,
 } from './inspect.js';
+export type {
+  ExploreKind,
+  ExploreTraceSource,
+  ExploreLogSource,
+  ExploreEntity,
+  ExploreWindow,
+  ExploreRequest,
+  ExploreResolved,
+  ExploreResponse,
+} from './explore.js';
 export {
   DslDebuggingClient,
   DEBUG_CATALOGS,


Reply via email to