From 571e23910e0b70dcecbd2bc17c55a869f8f335ad Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Tue, 2 Jun 2026 12:23:57 +0530 Subject: [PATCH 1/4] feat(ai-assistant): show descriptive Noz hover tooltip on all entry points (#11526) Noz launched under early access and its three entry points (header button, floating trigger, sidebar nav item) previously showed a bare "Noz" tooltip, which does not educate users who don't yet recognize the name. Replace it with a shared descriptive tooltip ("Noz, your AI teammate") sourced from a single constant so the surfaces never drift. - Add NOZ_TOOLTIP_TITLE in components/Noz/Noz.constants.ts - Header button and floating trigger read the constant - Add optional tooltip field to SidebarItem; NavItem wraps the whole row in a Tooltip (non-pinnable items only, to avoid nesting with the pin tooltip) - Move the trigger's Noz icon to the Button prefix slot to match the codebase convention for icon-only buttons --- .../HeaderRightSection/HeaderRightSection.tsx | 3 ++- frontend/src/components/Noz/Noz.constants.ts | 2 ++ .../AIAssistantTrigger/AIAssistantTrigger.tsx | 8 ++++---- frontend/src/container/SideNav/NavItem/NavItem.tsx | 14 +++++++++++--- frontend/src/container/SideNav/menuItems.tsx | 2 ++ frontend/src/container/SideNav/sideNav.types.ts | 2 ++ 6 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/Noz/Noz.constants.ts diff --git a/frontend/src/components/HeaderRightSection/HeaderRightSection.tsx b/frontend/src/components/HeaderRightSection/HeaderRightSection.tsx index 288cd288145..20facefc09c 100644 --- a/frontend/src/components/HeaderRightSection/HeaderRightSection.tsx +++ b/frontend/src/components/HeaderRightSection/HeaderRightSection.tsx @@ -4,6 +4,7 @@ import { Dot } from '@signozhq/icons'; import { Button } from '@signozhq/ui/button'; import { TooltipSimple } from '@signozhq/ui/tooltip'; import Noz from 'components/Noz/Noz'; +import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants'; import { Popover } from 'antd'; import logEvent from 'api/common/logEvent'; import { AIAssistantEvents } from 'container/AIAssistant/events'; @@ -109,7 +110,7 @@ function HeaderRightSection({ ) : null} - + + prefix={} + /> ); } diff --git a/frontend/src/container/SideNav/NavItem/NavItem.tsx b/frontend/src/container/SideNav/NavItem/NavItem.tsx index 958c7bd0561..4ed8830fe78 100644 --- a/frontend/src/container/SideNav/NavItem/NavItem.tsx +++ b/frontend/src/container/SideNav/NavItem/NavItem.tsx @@ -5,7 +5,6 @@ import { Pin, PinOff } from '@signozhq/icons'; import { SidebarItem } from '../sideNav.types'; -import './NavItem.styles.scss'; import './NavItem.styles.scss'; export default function NavItem({ @@ -27,7 +26,7 @@ export default function NavItem({ showIcon?: boolean; dataTestId?: string; }): JSX.Element { - const { label, icon, isBeta, isNew, isEarlyAccess } = item; + const { label, icon, isBeta, isNew, isEarlyAccess, tooltip } = item; const handleTogglePinClick = ( event: React.MouseEvent, @@ -36,7 +35,7 @@ export default function NavItem({ onTogglePin?.(item); }; - return ( + const navItem = (
); + + // Only non-pinnable items set `tooltip`; it would nest with the pin tooltip. + return tooltip ? ( + + {navItem} + + ) : ( + navItem + ); } NavItem.defaultProps = { diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index 86751ba40b8..4800ca3caa6 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -45,6 +45,7 @@ import { } from './sideNav.types'; import { Style } from '@signozhq/design-tokens'; import Noz from 'components/Noz/Noz'; +import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants'; export const getStartedMenuItem = { key: ROUTES.GET_STARTED, @@ -97,6 +98,7 @@ export const aiAssistantMenuItem = { icon: , itemKey: 'ai-assistant', isEarlyAccess: true, + tooltip: NOZ_TOOLTIP_TITLE, }; export const shortcutMenuItem = { diff --git a/frontend/src/container/SideNav/sideNav.types.ts b/frontend/src/container/SideNav/sideNav.types.ts index 9e2dd371ff8..e0d06e1d722 100644 --- a/frontend/src/container/SideNav/sideNav.types.ts +++ b/frontend/src/container/SideNav/sideNav.types.ts @@ -15,6 +15,8 @@ export interface SidebarItem { isBeta?: boolean; isNew?: boolean; isEarlyAccess?: boolean; + /** Hover copy for the whole item row (e.g. Noz's early-access tagline). */ + tooltip?: ReactNode; isPinned?: boolean; children?: SidebarItem[]; isExternal?: boolean; From 9074208b092eb6c45ea44b6052fc06a16bfafd62 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Tue, 2 Jun 2026 14:11:39 +0530 Subject: [PATCH 2/4] feat: added trace events (#11538) --- .../AnalyticsPanel/AnalyticsPanel.tsx | 4 +- .../SpanDetailsPanel/SpanDetailsPanel.tsx | 17 +++- .../TraceDetailsHeader/TraceDetailsHeader.tsx | 33 ++++++- frontend/src/pages/TraceDetailsV3/events.ts | 38 ++++++++ .../__tests__/useTraceDetailLogEvent.test.tsx | 88 +++++++++++++++++++ .../hooks/useTraceDetailLogEvent.ts | 39 ++++++++ frontend/src/pages/TraceDetailsV3/index.tsx | 57 ++++++++++++ .../components/DataViewer/DataViewer.tsx | 19 +++- 8 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 frontend/src/pages/TraceDetailsV3/events.ts create mode 100644 frontend/src/pages/TraceDetailsV3/hooks/__tests__/useTraceDetailLogEvent.test.tsx create mode 100644 frontend/src/pages/TraceDetailsV3/hooks/useTraceDetailLogEvent.ts diff --git a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel.tsx b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel.tsx index e9da5bd9ea5..fd845193ecf 100644 --- a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel.tsx +++ b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel.tsx @@ -22,6 +22,7 @@ import styles from './AnalyticsPanel.module.scss'; interface AnalyticsPanelProps { isOpen: boolean; onClose: () => void; + onTabChange: (tab: string) => void; } const PANEL_WIDTH = 350; @@ -32,6 +33,7 @@ const PANEL_MARGIN_BOTTOM = 50; function AnalyticsPanel({ isOpen, onClose, + onTabChange, }: AnalyticsPanelProps): JSX.Element | null { const aggregations = useTraceStore((s) => s.aggregations); const colorByFieldName = useTraceStore((s) => s.colorByField.name); @@ -118,7 +120,7 @@ function AnalyticsPanel({ />
- + % exec time diff --git a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx index f0aa945e6bd..ef9e1ef054d 100644 --- a/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx +++ b/frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanDetailsPanel.tsx @@ -31,7 +31,12 @@ import Events from 'container/SpanDetailsDrawer/Events/Events'; import SpanLogs from 'container/SpanDetailsDrawer/SpanLogs/SpanLogs'; import { useSpanContextLogs } from 'container/SpanDetailsDrawer/SpanLogs/useSpanContextLogs'; import dayjs from 'dayjs'; +import { + TraceDetailEventKeys, + TraceDetailEvents, +} from 'pages/TraceDetailsV3/events'; import { useMigratePinnedAttributes } from 'pages/TraceDetailsV3/hooks/useMigratePinnedAttributes'; +import { useTraceDetailLogEvent } from 'pages/TraceDetailsV3/hooks/useTraceDetailLogEvent'; import { getSpanAttribute, getSpanDisplayData, @@ -86,6 +91,16 @@ function SpanDetailsContent({ }): JSX.Element { const FIVE_MINUTES_IN_MS = 5 * 60 * 1000; const spanAttributeActions = useSpanAttributeActions(); + const logTraceEvent = useTraceDetailLogEvent('v3', selectedSpan.trace_id); + const handleTabChange = useCallback( + (tab: string): void => { + logTraceEvent(TraceDetailEvents.SpanPanelTabChanged, { + [TraceDetailEventKeys.Tab]: tab, + [TraceDetailEventKeys.SpanId]: selectedSpan.span_id, + }); + }, + [logTraceEvent, selectedSpan.span_id], + ); const percentile = useSpanPercentile(selectedSpan); const linkedSpans = useLinkedSpans((selectedSpan as any).references); @@ -376,7 +391,7 @@ function SpanDetailsContent({
{/* Step 9: ContentTabs */} - + Overview diff --git a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx index a634bcb6d22..bb1c32bf1d3 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx +++ b/frontend/src/pages/TraceDetailsV3/TraceDetailsHeader/TraceDetailsHeader.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Button } from '@signozhq/ui/button'; import { @@ -29,6 +29,8 @@ import KeyValueLabel from 'periscope/components/KeyValueLabel'; import { TraceDetailV2URLProps } from 'types/api/trace/getTraceV2'; import { DataSource } from 'types/common/queryBuilder'; +import { TraceDetailEventKeys, TraceDetailEvents } from '../events'; +import { useTraceDetailLogEvent } from '../hooks/useTraceDetailLogEvent'; import { useTraceStore } from '../stores/traceStore'; import AnalyticsPanel from '../SpanDetailsPanel/AnalyticsPanel/AnalyticsPanel'; import Filters from '../TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters'; @@ -90,11 +92,35 @@ function TraceDetailsHeader({ const previewFields = useTraceStore((s) => s.previewFields); const setPreviewFields = useTraceStore((s) => s.setPreviewFields); + const logTraceEvent = useTraceDetailLogEvent('v3', traceID || ''); + const pageLoadedAtRef = useRef(Date.now()); + const handleSwitchToOldView = useCallback((): void => { + logTraceEvent(TraceDetailEvents.ViewSwitched, { + [TraceDetailEventKeys.From]: 'v3', + [TraceDetailEventKeys.To]: 'v2', + [TraceDetailEventKeys.DwellMs]: Date.now() - pageLoadedAtRef.current, + }); setLocalStorageKey(LOCALSTORAGE.TRACE_DETAILS_PREFER_OLD_VIEW, 'true'); const oldUrl = `/trace-old/${traceID}${window.location.search}`; history.replace(oldUrl); - }, [traceID]); + }, [traceID, logTraceEvent]); + + const handleToggleAnalytics = useCallback((): void => { + logTraceEvent(TraceDetailEvents.AnalyticsPanelToggled, { + [TraceDetailEventKeys.Open]: !isAnalyticsOpen, + }); + setIsAnalyticsOpen((prev) => !prev); + }, [logTraceEvent, isAnalyticsOpen]); + + const handleAnalyticsTabChange = useCallback( + (tab: string): void => { + logTraceEvent(TraceDetailEvents.AnalyticsTabChanged, { + [TraceDetailEventKeys.Tab]: tab, + }); + }, + [logTraceEvent], + ); const handlePreviousBtnClick = useCallback((): void => { if (hasInAppHistory()) { @@ -167,7 +193,7 @@ function TraceDetailsHeader({ size="icon" color="secondary" aria-label="Analytics" - onClick={(): void => setIsAnalyticsOpen((prev) => !prev)} + onClick={handleToggleAnalytics} > @@ -245,6 +271,7 @@ function TraceDetailsHeader({ setIsAnalyticsOpen(false)} + onTabChange={handleAnalyticsTabChange} />
); diff --git a/frontend/src/pages/TraceDetailsV3/events.ts b/frontend/src/pages/TraceDetailsV3/events.ts new file mode 100644 index 00000000000..8561a902ac3 --- /dev/null +++ b/frontend/src/pages/TraceDetailsV3/events.ts @@ -0,0 +1,38 @@ +export enum TraceDetailEvents { + DataLoaded = 'Trace Detail: Data loaded', + ViewSwitched = 'Trace Detail: View switched', + FlameGraphToggled = 'Trace Detail: Flame graph toggled', + WaterfallToggled = 'Trace Detail: Waterfall toggled', + AnalyticsPanelToggled = 'Trace Detail: Analytics panel toggled', + AnalyticsTabChanged = 'Trace Detail: Analytics tab changed', + SpanPanelTabChanged = 'Trace Detail: Span panel tab changed', +} + +export enum TraceDetailEventKeys { + // Injected on every event by useTraceDetailLogEvent + View = 'view', + TraceId = 'traceId', + // Data loaded — trace shape + TotalSpansCount = 'totalSpansCount', + NumServices = 'numServices', + TraceDurationMs = 'traceDurationMs', + HadErrors = 'hadErrors', + FlamegraphSampled = 'flamegraphSampled', + // Data loaded — persisted settings + SpanPanelVariant = 'spanPanelVariant', + ColorByField = 'colorByField', + PreviewFieldsCount = 'previewFieldsCount', + EntryPreferOldView = 'entryPreferOldView', + // View switched + From = 'from', + To = 'to', + DwellMs = 'dwellMs', + // Toggles / tabs + Expanded = 'expanded', + Open = 'open', + Tab = 'tab', + // Span panel tab changed + SpanId = 'spanId', +} + +export type TraceDetailView = 'v2' | 'v3'; diff --git a/frontend/src/pages/TraceDetailsV3/hooks/__tests__/useTraceDetailLogEvent.test.tsx b/frontend/src/pages/TraceDetailsV3/hooks/__tests__/useTraceDetailLogEvent.test.tsx new file mode 100644 index 00000000000..682e036ed09 --- /dev/null +++ b/frontend/src/pages/TraceDetailsV3/hooks/__tests__/useTraceDetailLogEvent.test.tsx @@ -0,0 +1,88 @@ +import { act, renderHook } from '@testing-library/react'; + +import { TraceDetailEvents } from '../../events'; +import { useTraceDetailLogEvent } from '../useTraceDetailLogEvent'; + +const logEventMock = jest.fn(); + +jest.mock('api/common/logEvent', () => ({ + __esModule: true, + default: (...args: unknown[]): void => logEventMock(...args), +})); + +describe('useTraceDetailLogEvent', () => { + beforeEach(() => { + logEventMock.mockClear(); + }); + + it('injects view and traceId on every event', () => { + const { result } = renderHook(() => + useTraceDetailLogEvent('v3', 'trace-123'), + ); + + act(() => { + result.current(TraceDetailEvents.DataLoaded, { totalSpansCount: 42 }); + }); + + expect(logEventMock).toHaveBeenCalledTimes(1); + expect(logEventMock).toHaveBeenCalledWith(TraceDetailEvents.DataLoaded, { + view: 'v3', + traceId: 'trace-123', + totalSpansCount: 42, + }); + }); + + it('injects view and traceId even when no attributes are passed', () => { + const { result } = renderHook(() => + useTraceDetailLogEvent('v2', 'trace-456'), + ); + + act(() => { + result.current(TraceDetailEvents.ViewSwitched); + }); + + expect(logEventMock).toHaveBeenCalledWith(TraceDetailEvents.ViewSwitched, { + view: 'v2', + traceId: 'trace-456', + }); + }); + + it('keeps a stable callback identity and emits the latest traceId', () => { + const { result, rerender } = renderHook( + ({ traceId }) => useTraceDetailLogEvent('v3', traceId), + { initialProps: { traceId: 'trace-1' } }, + ); + + const firstIdentity = result.current; + rerender({ traceId: 'trace-2' }); + + expect(result.current).toBe(firstIdentity); + + act(() => { + result.current(TraceDetailEvents.SpanPanelTabChanged, { spanId: 's1' }); + }); + expect(logEventMock).toHaveBeenCalledWith( + TraceDetailEvents.SpanPanelTabChanged, + { + view: 'v3', + traceId: 'trace-2', + spanId: 's1', + }, + ); + }); + + it('never throws if logEvent throws (analytics must not break the UI)', () => { + logEventMock.mockImplementationOnce(() => { + throw new Error('network down'); + }); + const { result } = renderHook(() => + useTraceDetailLogEvent('v3', 'trace-123'), + ); + + expect(() => { + act(() => { + result.current(TraceDetailEvents.DataLoaded); + }); + }).not.toThrow(); + }); +}); diff --git a/frontend/src/pages/TraceDetailsV3/hooks/useTraceDetailLogEvent.ts b/frontend/src/pages/TraceDetailsV3/hooks/useTraceDetailLogEvent.ts new file mode 100644 index 00000000000..758c8013e2b --- /dev/null +++ b/frontend/src/pages/TraceDetailsV3/hooks/useTraceDetailLogEvent.ts @@ -0,0 +1,39 @@ +import { useCallback, useRef } from 'react'; +import logEvent from 'api/common/logEvent'; + +import { + TraceDetailEventKeys, + TraceDetailEvents, + TraceDetailView, +} from '../events'; + +export type TraceDetailLogEvent = ( + event: TraceDetailEvents, + attributes?: Record, +) => void; + +export function useTraceDetailLogEvent( + view: TraceDetailView, + traceId: string, +): TraceDetailLogEvent { + const contextRef = useRef({ view, traceId }); + contextRef.current = { view, traceId }; + + return useCallback( + ( + event: TraceDetailEvents, + attributes: Record = {}, + ): void => { + try { + void logEvent(event, { + [TraceDetailEventKeys.View]: contextRef.current.view, + [TraceDetailEventKeys.TraceId]: contextRef.current.traceId, + ...attributes, + }); + } catch { + // No-op. Logging must never throw into the UI. + } + }, + [], + ); +} diff --git a/frontend/src/pages/TraceDetailsV3/index.tsx b/frontend/src/pages/TraceDetailsV3/index.tsx index 1757c955013..70fc91141be 100644 --- a/frontend/src/pages/TraceDetailsV3/index.tsx +++ b/frontend/src/pages/TraceDetailsV3/index.tsx @@ -20,7 +20,10 @@ import { } from 'types/api/trace/getTraceV3'; import { COLOR_BY_FIELDS } from './constants'; +import { TraceDetailEventKeys, TraceDetailEvents } from './events'; +import { useTraceDetailLogEvent } from './hooks/useTraceDetailLogEvent'; import TraceStoreSync from './stores/TraceStoreSync'; +import { useTraceStore } from './stores/traceStore'; import { AGGREGATIONS } from './utils/aggregations'; import { SpanDetailVariant } from './SpanDetailsPanel/constants'; import SpanDetailsPanel from './SpanDetailsPanel/SpanDetailsPanel'; @@ -56,6 +59,14 @@ function TraceDetailsV3(): JSX.Element { const selectedSpanId = urlQuery.get('spanId') || undefined; const { safeNavigate } = useSafeNavigate(); + const logTraceEvent = useTraceDetailLogEvent('v3', traceId || ''); + // Tracks which traceId the load event already fired for, so navigating + // between traces (the route component stays mounted) re-fires it once each. + const dataLoadedFiredForRef = useRef(''); + const colorByField = useTraceStore((s) => s.colorByField); + const previewFieldsCount = useTraceStore((s) => s.previewFields.length); + const userPrefsReady = useTraceStore((s) => s.userPreferences !== null); + const handleSpanDetailsClose = useCallback((): void => { urlQuery.delete('spanId'); safeNavigate({ search: urlQuery.toString() }); @@ -154,6 +165,46 @@ function TraceDetailsV3(): JSX.Element { allSpansRef.current = allSpans; }, [allSpans]); + useEffect(() => { + if ( + !traceId || + dataLoadedFiredForRef.current === traceId || + !userPrefsReady + ) { + return; + } + const payload = traceData?.payload; + if (!payload?.spans?.length) { + return; + } + dataLoadedFiredForRef.current = traceId; + const numServices = new Set(payload.spans.map((s) => s['service.name'])).size; + logTraceEvent(TraceDetailEvents.DataLoaded, { + [TraceDetailEventKeys.TotalSpansCount]: totalSpansCount, + [TraceDetailEventKeys.NumServices]: numServices, + [TraceDetailEventKeys.TraceDurationMs]: + payload.endTimestampMillis - payload.startTimestampMillis, + [TraceDetailEventKeys.HadErrors]: (payload.totalErrorSpansCount || 0) > 0, + [TraceDetailEventKeys.FlamegraphSampled]: + totalSpansCount > FLAMEGRAPH_SPAN_LIMIT, + [TraceDetailEventKeys.SpanPanelVariant]: + getLocalStorageKey(LOCALSTORAGE.TRACE_DETAILS_SPAN_DETAILS_POSITION) || + SpanDetailVariant.DOCKED_RIGHT, + [TraceDetailEventKeys.ColorByField]: colorByField.name, + [TraceDetailEventKeys.PreviewFieldsCount]: previewFieldsCount, + [TraceDetailEventKeys.EntryPreferOldView]: + getLocalStorageKey(LOCALSTORAGE.TRACE_DETAILS_PREFER_OLD_VIEW) === 'true', + }); + }, [ + traceId, + userPrefsReady, + traceData, + totalSpansCount, + colorByField, + previewFieldsCount, + logTraceEvent, + ]); + // Frontend mode: expand all parents by default when full data arrives useEffect(() => { if (isFullDataLoaded && allSpans.length > 0) { @@ -233,6 +284,12 @@ function TraceDetailsV3(): JSX.Element { const [activeKeys, setActiveKeys] = useState(['flame', 'waterfall']); const handleCollapseChange = (key: string): void => { + logTraceEvent( + key === 'flame' + ? TraceDetailEvents.FlameGraphToggled + : TraceDetailEvents.WaterfallToggled, + { [TraceDetailEventKeys.Expanded]: !activeKeys.includes(key) }, + ); setActiveKeys((prev) => prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key], ); diff --git a/frontend/src/periscope/components/DataViewer/DataViewer.tsx b/frontend/src/periscope/components/DataViewer/DataViewer.tsx index 0f9b1d46187..4dba34589d6 100644 --- a/frontend/src/periscope/components/DataViewer/DataViewer.tsx +++ b/frontend/src/periscope/components/DataViewer/DataViewer.tsx @@ -4,6 +4,7 @@ import { ChevronDown, Copy } from '@signozhq/icons'; import { Button } from '@signozhq/ui/button'; import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu'; import { toast } from '@signozhq/ui/sonner'; +import logEvent from 'api/common/logEvent'; import { JsonView } from 'periscope/components/JsonView'; import { PrettyView } from 'periscope/components/PrettyView'; import { PrettyViewProps } from 'periscope/components/PrettyView'; @@ -12,6 +13,8 @@ import './DataViewer.styles.scss'; type ViewMode = 'pretty' | 'json'; +const VIEW_MODE_CHANGED_EVENT = 'Data Viewer: View mode changed'; + const VIEW_MODE_OPTIONS: { label: string; value: ViewMode }[] = [ { label: 'Pretty', value: 'pretty' }, { label: 'JSON', value: 'json' }, @@ -34,6 +37,20 @@ function DataViewer({ const jsonString = useMemo(() => JSON.stringify(data, null, 2), [data]); + const handleViewModeChange = (value: string): void => { + const next = value as ViewMode; + setViewMode(next); + try { + logEvent(VIEW_MODE_CHANGED_EVENT, { + viewMode: next, + path: window.location.pathname, + drawerKey, + }); + } catch { + // No op + } + }; + const handleCopy = (): void => { const text = JSON.stringify(data, null, 2); setCopy(text); @@ -56,7 +73,7 @@ function DataViewer({ { type: 'radio-group', value: viewMode, - onChange: (value): void => setViewMode(value as ViewMode), + onChange: handleViewModeChange, children: VIEW_MODE_OPTIONS.map((opt) => ({ type: 'radio', key: opt.value, From e43aeb8e249b3ac6e91ce156edfb4c74186ac909 Mon Sep 17 00:00:00 2001 From: Nikhil Soni Date: Tue, 2 Jun 2026 14:12:26 +0530 Subject: [PATCH 3/4] fix: add references in waterfall response (#11536) * fix: add references in waterfall response * chore: update openapi specs * chore: mark reference a non nullable field --- docs/api/openapi.yml | 15 +++++++++++++++ .../api/generated/services/sigNoz.schemas.ts | 19 +++++++++++++++++++ .../tracedetail/impltracedetail/store.go | 4 ++-- pkg/types/spantypes/waterfall_span.go | 17 +++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index 6f7fc074a86..33df786a49c 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -6572,6 +6572,15 @@ components: nullable: true type: array type: object + SpantypesOtelSpanRef: + properties: + refType: + type: string + spanId: + type: string + traceId: + type: string + type: object SpantypesPostableSpanMapper: properties: config: @@ -6835,6 +6844,10 @@ components: type: string parent_span_id: type: string + references: + items: + $ref: '#/components/schemas/SpantypesOtelSpanRef' + type: array resource: additionalProperties: type: string @@ -6860,6 +6873,8 @@ components: type: string trace_state: type: string + required: + - references type: object TagtypesPostableTag: properties: diff --git a/frontend/src/api/generated/services/sigNoz.schemas.ts b/frontend/src/api/generated/services/sigNoz.schemas.ts index f8a8d490631..6158ba19de2 100644 --- a/frontend/src/api/generated/services/sigNoz.schemas.ts +++ b/frontend/src/api/generated/services/sigNoz.schemas.ts @@ -7768,6 +7768,21 @@ export interface SpantypesGettableTraceAggregationsDTO { aggregations: SpantypesSpanAggregationResultDTO[]; } +export interface SpantypesOtelSpanRefDTO { + /** + * @type string + */ + refType?: string; + /** + * @type string + */ + spanId?: string; + /** + * @type string + */ + traceId?: string; +} + export type SpantypesWaterfallSpanDTOAttributesAnyOf = { [key: string]: unknown; }; @@ -7862,6 +7877,10 @@ export interface SpantypesWaterfallSpanDTO { * @type string */ parent_span_id?: string; + /** + * @type array + */ + references: SpantypesOtelSpanRefDTO[]; /** * @type object,null */ diff --git a/pkg/modules/tracedetail/impltracedetail/store.go b/pkg/modules/tracedetail/impltracedetail/store.go index 0470f684e7b..11067defb3d 100644 --- a/pkg/modules/tracedetail/impltracedetail/store.go +++ b/pkg/modules/tracedetail/impltracedetail/store.go @@ -74,7 +74,7 @@ func (s *traceStore) GetTraceSpans(ctx context.Context, traceID string, summary events, status_message, status_code_string, kind_string, parent_span_id, flags, is_remote, trace_state, status_code, db_name, db_operation, http_method, http_url, http_host, - external_http_method, external_http_url, response_status_code + external_http_method, external_http_url, response_status_code, links as references FROM %s.%s WHERE trace_id=? AND ts_bucket_start>=? AND ts_bucket_start<=? ORDER BY timestamp ASC, name ASC`, @@ -130,7 +130,7 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta "events", "status_message", "status_code_string", "kind_string", "parent_span_id", "flags", "is_remote", "trace_state", "status_code", "db_name", "db_operation", "http_method", "http_url", "http_host", - "external_http_method", "external_http_url", "response_status_code", + "external_http_method", "external_http_url", "response_status_code", "links as references", ) sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable)) ids := make([]any, len(spanIDs)) diff --git a/pkg/types/spantypes/waterfall_span.go b/pkg/types/spantypes/waterfall_span.go index 173675e1016..7e47276b125 100644 --- a/pkg/types/spantypes/waterfall_span.go +++ b/pkg/types/spantypes/waterfall_span.go @@ -54,6 +54,12 @@ type Event struct { IsError bool `json:"isError,omitempty"` } +type OtelSpanRef struct { + TraceId string `json:"traceId,omitempty"` + SpanId string `json:"spanId,omitempty"` + RefType string `json:"refType,omitempty"` +} + // WaterfallSpan represents the span in waterfall response, // this uses snake_case keys for response as a special case since these // keys can be directly used to query spans and client need to know the actual fields. @@ -74,6 +80,7 @@ type WaterfallSpan struct { TimeUnix uint64 `json:"time_unix"` TraceID string `json:"trace_id"` TraceState string `json:"trace_state"` + References []OtelSpanRef `json:"references" required:"true" nullable:"false"` // Calculated fields https://signoz.io/docs/traces-management/guides/derived-fields-spans DBName string `json:"db_name,omitempty"` @@ -128,6 +135,7 @@ type StorableSpan struct { ExternalHTTPMethod string `ch:"external_http_method"` ExternalHTTPURL string `ch:"external_http_url"` ResponseStatusCode string `ch:"response_status_code"` + References string `ch:"references"` } // MinimalSpan with only the fields needed to build the parent-child tree. @@ -285,6 +293,14 @@ func (item *StorableSpan) UnmarshalledEvents() []Event { return events } +func (item *StorableSpan) UnmarshalledRefs() []OtelSpanRef { + refs := []OtelSpanRef{} + if err := json.Unmarshal([]byte(item.References), &refs); err != nil { + return nil // skip malformed values + } + return refs +} + func (item *StorableSpan) ToWaterfallSpan(traceID string) *WaterfallSpan { resources := make(map[string]string) maps.Copy(resources, item.ResourcesString) @@ -318,6 +334,7 @@ func (item *StorableSpan) ToWaterfallSpan(traceID string) *WaterfallSpan { Children: make([]*WaterfallSpan, 0), TimeUnix: uint64(item.StartTime.UnixNano()), ServiceName: item.ServiceName, + References: item.UnmarshalledRefs(), } } From 0963ff08cdec5d046ef0a265c2fb76d332a7af20 Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Tue, 2 Jun 2026 15:02:20 +0530 Subject: [PATCH 4/4] feat(web): disable all integrations by default (#11539) --- conf/example.yaml | 8 ++++---- pkg/web/config.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conf/example.yaml b/conf/example.yaml index c6a3b7dbc80..2a7166b12b9 100644 --- a/conf/example.yaml +++ b/conf/example.yaml @@ -64,16 +64,16 @@ web: settings: posthog: # Whether to enable PostHog in web. - enabled: true + enabled: false appcues: # Whether to enable Appcues in web. - enabled: true + enabled: false sentry: # Whether to enable Sentry in web. - enabled: true + enabled: false pylon: # Whether to enable Pylon in web. - enabled: true + enabled: false ##################### Cache ##################### cache: diff --git a/pkg/web/config.go b/pkg/web/config.go index 0a3f7109184..bf90b92c6f5 100644 --- a/pkg/web/config.go +++ b/pkg/web/config.go @@ -54,16 +54,16 @@ func newConfig() factory.Config { Directory: "/etc/signoz/web", Settings: SettingsConfig{ Posthog: PosthogConfig{ - Enabled: true, + Enabled: false, }, Appcues: AppcuesConfig{ - Enabled: true, + Enabled: false, }, Sentry: SentryConfig{ - Enabled: true, + Enabled: false, }, Pylon: PylonConfig{ - Enabled: true, + Enabled: false, }, }, }