Skip to content

Commit 5d22d69

Browse files
k-fishlzhao-sentry
authored andcommitted
feat(ourlogs): Add pii scrubbing reason tooltip to logs scrubbed fields (#98120)
### Summary This adds the scrubbing reason as a tooltip when hovering the 'Filtered' etc. scrubbed text #### Screenshots <img width="431" height="243" alt="Screenshot 2025-08-21 at 6 38 54 PM" src="https://github.com/user-attachments/assets/7a4a1bb5-bbda-4df0-84ca-84042124c630" /> Closes LOGS-81
1 parent b4b53c9 commit 5d22d69

File tree

11 files changed

+424
-81
lines changed

11 files changed

+424
-81
lines changed

static/app/utils/discover/fieldRenderers.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
5757
import {isUrl} from 'sentry/utils/string/isUrl';
5858
import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
5959
import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
60+
import type {TraceItemDetailsMeta} from 'sentry/views/explore/hooks/useTraceItemDetails';
6061
import {PerformanceBadge} from 'sentry/views/insights/browser/webVitals/components/performanceBadge';
6162
import {PercentChangeCell} from 'sentry/views/insights/common/components/tableCells/percentChangeCell';
6263
import {ResponseStatusCodeCell} from 'sentry/views/insights/common/components/tableCells/responseStatusCodeCell';
@@ -105,6 +106,11 @@ export type RenderFunctionBaggage = {
105106
disableLazyLoad?: boolean;
106107
eventView?: EventView;
107108
projectSlug?: string;
109+
/**
110+
* The trace item meta data for the trace item, which includes information needed to render annotated tooltip (eg. scrubbing reasons)
111+
*/
112+
traceItemMeta?: TraceItemDetailsMeta;
113+
108114
unit?: string;
109115
};
110116

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
3+
import {Tooltip} from 'sentry/components/core/tooltip';
4+
import type {Project} from 'sentry/types/project';
5+
import {useApiQuery} from 'sentry/utils/queryClient';
6+
import type {RendererExtra} from 'sentry/views/explore/logs/fieldRenderers';
7+
import {TraceItemMetaInfo} from 'sentry/views/explore/utils';
8+
9+
export function AnnotatedAttributeTooltip({
10+
children,
11+
extra,
12+
fieldKey,
13+
}: {
14+
children: React.ReactNode;
15+
extra: Pick<RendererExtra, 'traceItemMeta' | 'organization' | 'project'>;
16+
fieldKey?: string;
17+
}) {
18+
// Fetch full project details including `project.relayPiiConfig`
19+
// That property is not normally available in the store.
20+
// Taken from FilteredAnnotatedTextValue.tsx
21+
const {data: projectDetails} = useApiQuery<Project>(
22+
[`/projects/${extra.organization.slug}/${extra.project?.slug}/`],
23+
{
24+
staleTime: Infinity,
25+
retry: false,
26+
enabled: !!extra.project?.slug && !!fieldKey && !!extra.traceItemMeta,
27+
notifyOnChangeProps: ['data'],
28+
}
29+
);
30+
31+
// Check if there's meta information for this field
32+
if (!fieldKey || !extra.traceItemMeta) {
33+
return <React.Fragment>{children}</React.Fragment>;
34+
}
35+
36+
const metaTooltip = TraceItemMetaInfo.getTooltipText(
37+
fieldKey,
38+
extra.traceItemMeta,
39+
extra.organization,
40+
projectDetails
41+
);
42+
43+
if (!metaTooltip) {
44+
return <React.Fragment>{children}</React.Fragment>;
45+
}
46+
47+
return (
48+
<Tooltip title={metaTooltip} isHoverable>
49+
{children}
50+
</Tooltip>
51+
);
52+
}

static/app/views/explore/components/traceItemAttributes/attributesTreeValue.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {openNavigateToExternalLinkModal} from 'sentry/actionCreators/modal';
55
import ExternalLink from 'sentry/components/links/externalLink';
66
import {type RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
77
import {isUrl} from 'sentry/utils/string/isUrl';
8+
import {AnnotatedAttributeTooltip} from 'sentry/views/explore/components/annotatedAttributeTooltip';
89
import {getAttributeItem} from 'sentry/views/explore/components/traceItemAttributes/utils';
10+
import {TraceItemMetaInfo} from 'sentry/views/explore/utils';
911

1012
import type {
1113
AttributesFieldRender,
@@ -45,6 +47,16 @@ export function AttributesTreeValue<RendererExtra extends RenderFunctionBaggage>
4547
});
4648
}
4749

50+
if (renderExtra.traceItemMeta) {
51+
const metaInfo = new TraceItemMetaInfo(renderExtra.traceItemMeta);
52+
if (metaInfo.hasRemarks(attributeKey)) {
53+
return (
54+
<AnnotatedAttributeTooltip fieldKey={attributeKey} extra={renderExtra}>
55+
{defaultValue}
56+
</AnnotatedAttributeTooltip>
57+
);
58+
}
59+
}
4860
return isUrl(String(content.value)) ? (
4961
<AttributeLinkText>
5062
<ExternalLink

static/app/views/explore/hooks/useTraceItemDetails.tsx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1+
import {useState} from 'react';
12
import {useHover} from '@react-aria/interactions';
23
import {captureException} from '@sentry/react';
34

4-
import {
5-
fetchDataQuery,
6-
useApiQuery,
7-
useQueryClient,
8-
type ApiQueryKey,
9-
} from 'sentry/utils/queryClient';
5+
import type {Meta} from 'sentry/types/group';
6+
import {useApiQuery, type ApiQueryKey} from 'sentry/utils/queryClient';
107
import useOrganization from 'sentry/utils/useOrganization';
118
import useProjectFromId from 'sentry/utils/useProjectFromId';
129
import type {TraceItemDataset} from 'sentry/views/explore/types';
@@ -43,9 +40,21 @@ interface UseTraceItemDetailsProps {
4340
enabled?: boolean;
4441
}
4542

43+
export type TraceItemAttributeMeta = Pick<Meta, 'len' | 'rem'>;
44+
interface TraceItemDetailsMetaRecord {
45+
meta: {
46+
value: {
47+
'': TraceItemAttributeMeta;
48+
};
49+
};
50+
}
51+
52+
export type TraceItemDetailsMeta = Record<string, TraceItemDetailsMetaRecord>;
53+
4654
export interface TraceItemDetailsResponse {
4755
attributes: TraceItemResponseAttribute[];
4856
itemId: string;
57+
meta: TraceItemDetailsMeta;
4958
timestamp: string;
5059
links?: TraceItemResponseLink[];
5160
}
@@ -137,7 +146,7 @@ function traceItemDetailsQueryKey({
137146
];
138147
}
139148

140-
export function usePrefetchTraceItemDetailsOnHover({
149+
export function useFetchTraceItemDetailsOnHover({
141150
traceItemId,
142151
projectId,
143152
traceId,
@@ -161,32 +170,23 @@ export function usePrefetchTraceItemDetailsOnHover({
161170
*/
162171
hoverPrefetchDisabled?: boolean;
163172
}) {
164-
const organization = useOrganization();
165-
const project = useProjectFromId({project_id: projectId});
166-
const queryClient = useQueryClient();
173+
const [timeoutReached, setTimeoutReached] = useState(false);
174+
const traceItemsResult = useTraceItemDetails({
175+
projectId,
176+
traceItemId,
177+
traceId,
178+
traceItemType,
179+
referrer,
180+
enabled: timeoutReached,
181+
});
167182

168183
const {hoverProps} = useHover({
169184
onHoverStart: () => {
170185
if (sharedHoverTimeoutRef.current) {
171186
clearTimeout(sharedHoverTimeoutRef.current);
172187
}
173188
sharedHoverTimeoutRef.current = setTimeout(() => {
174-
queryClient.prefetchQuery({
175-
queryKey: traceItemDetailsQueryKey({
176-
urlParams: {
177-
organizationSlug: organization.slug,
178-
projectSlug: project?.slug ?? '',
179-
traceItemId,
180-
},
181-
queryParams: {
182-
traceItemType,
183-
referrer,
184-
traceId,
185-
},
186-
}),
187-
queryFn: fetchDataQuery,
188-
staleTime: Infinity, // Prefetched items are never stale as the row is either entirely stored or not stored at all.
189-
});
189+
setTimeoutReached(true);
190190
}, timeout);
191191
},
192192
onHoverEnd: () => {
@@ -197,5 +197,8 @@ export function usePrefetchTraceItemDetailsOnHover({
197197
isDisabled: hoverPrefetchDisabled,
198198
});
199199

200-
return hoverProps;
200+
return {
201+
hoverProps,
202+
traceItemsResult,
203+
};
201204
}

static/app/views/explore/logs/fieldRenderers.tsx

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {DateTime} from 'sentry/components/dateTime';
77
import useStacktraceLink from 'sentry/components/events/interfaces/frame/useStacktraceLink';
88
import Version from 'sentry/components/version';
99
import {tct} from 'sentry/locale';
10+
import type {Project} from 'sentry/types/project';
1011
import {defined} from 'sentry/utils';
1112
import {stripAnsi} from 'sentry/utils/ansiEscapeCodes';
1213
import type {EventsMetaType} from 'sentry/utils/discover/eventView';
@@ -20,9 +21,13 @@ import normalizeUrl from 'sentry/utils/url/normalizeUrl';
2021
import {useRelease} from 'sentry/utils/useRelease';
2122
import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
2223
import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
24+
import {AnnotatedAttributeTooltip} from 'sentry/views/explore/components/annotatedAttributeTooltip';
2325
import type {AttributesFieldRendererProps} from 'sentry/views/explore/components/traceItemAttributes/attributesTree';
2426
import {stripLogParamsFromLocation} from 'sentry/views/explore/contexts/logs/logsPageParams';
25-
import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails';
27+
import type {
28+
TraceItemDetailsResponse,
29+
TraceItemResponseAttribute,
30+
} from 'sentry/views/explore/hooks/useTraceItemDetails';
2631
import {LOG_ATTRIBUTE_LAZY_LOAD_HOVER_TIMEOUT} from 'sentry/views/explore/logs/constants';
2732
import LogsTimestampTooltip from 'sentry/views/explore/logs/logsTimeTooltip';
2833
import {
@@ -44,10 +49,12 @@ import {
4449
import {
4550
adjustLogTraceID,
4651
getLogSeverityLevel,
52+
logOnceFactory,
4753
logsFieldAlignment,
4854
SeverityLevel,
4955
severityLevelToText,
5056
} from 'sentry/views/explore/logs/utils';
57+
import {TraceItemMetaInfo} from 'sentry/views/explore/utils';
5158
import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
5259
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
5360

@@ -62,8 +69,11 @@ export interface RendererExtra extends RenderFunctionBaggage {
6269
highlightTerms: string[];
6370
logColors: ReturnType<typeof getLogColors>;
6471
align?: 'left' | 'center' | 'right';
72+
meta?: EventsMetaType;
73+
project?: Project;
6574
projectSlug?: string;
6675
shouldRenderHoverElements?: boolean;
76+
traceItemMeta?: TraceItemDetailsResponse['meta'];
6777
useFullSeverityText?: boolean;
6878
wrapBody?: true;
6979
}
@@ -250,6 +260,11 @@ function useLazyLoadAttributeOnHover(hoverTimeout: number, props: LogFieldRender
250260
};
251261
}
252262

263+
/**
264+
* This should only wrap the 'body' attribute as it is the only 'field' that is not returned by the trace-items endpoint (since it is not an attribute but rather a field).
265+
*
266+
* Once the trace-items endpoint returns 'body', we can remove this component.
267+
*/
253268
function FilteredTooltip({
254269
value,
255270
children,
@@ -346,13 +361,11 @@ export function LogBodyRenderer(props: LogFieldRendererProps) {
346361

347362
function LogTemplateRenderer(props: LogFieldRendererProps) {
348363
return (
349-
<FilteredTooltip value={props.item.value} extra={props.extra}>
350-
<span>
351-
{typeof props.item.value === 'string'
352-
? stripAnsi(props.item.value)
353-
: props.basicRendered}
354-
</span>
355-
</FilteredTooltip>
364+
<span>
365+
{typeof props.item.value === 'string'
366+
? stripAnsi(props.item.value)
367+
: props.basicRendered}
368+
</span>
356369
);
357370
}
358371

@@ -399,16 +412,59 @@ export function LogFieldRenderer(props: LogFieldRendererProps) {
399412
const align = logsFieldAlignment(adjustedFieldKey, type);
400413

401414
if (!customRenderer) {
402-
return <Fragment>{basicRendered}</Fragment>;
415+
return (
416+
<AnnotatedAttributeWrapper extra={props.extra} fieldKey={props.item.fieldKey}>
417+
{basicRendered}
418+
</AnnotatedAttributeWrapper>
419+
);
403420
}
404421

405422
return (
406-
<AlignedCellContent align={align}>
407-
{customRenderer({...props, basicRendered})}
408-
</AlignedCellContent>
423+
<AnnotatedAttributeWrapper extra={props.extra} fieldKey={props.item.fieldKey}>
424+
<AlignedCellContent align={align}>
425+
{customRenderer({...props, basicRendered})}
426+
</AlignedCellContent>
427+
</AnnotatedAttributeWrapper>
409428
);
410429
}
411430

431+
const logInfoOnceHasRemarks = logOnceFactory('info');
432+
433+
function AnnotatedAttributeWrapper(props: {
434+
children: React.ReactNode;
435+
extra: RendererExtra;
436+
fieldKey: string;
437+
}) {
438+
if (props.extra.traceItemMeta) {
439+
const metaInfo = new TraceItemMetaInfo(props.extra.traceItemMeta);
440+
if (metaInfo.hasRemarks(props.fieldKey)) {
441+
try {
442+
logInfoOnceHasRemarks(
443+
`AnnotatedAttributeWrapper: ${props.fieldKey} has remarks, rendering tooltip`,
444+
{
445+
organization: props.extra.organization,
446+
project: props.extra.project,
447+
text: TraceItemMetaInfo.getTooltipText(
448+
props.fieldKey,
449+
props.extra.traceItemMeta,
450+
props.extra.organization,
451+
props.extra.project
452+
),
453+
}
454+
);
455+
} catch {
456+
// defensive
457+
}
458+
return (
459+
<AnnotatedAttributeTooltip extra={props.extra} fieldKey={props.fieldKey}>
460+
{props.children}
461+
</AnnotatedAttributeTooltip>
462+
);
463+
}
464+
}
465+
return props.children;
466+
}
467+
412468
/**
413469
* Only formats the field the same as discover does, does not apply any additional rendering, but has a container to fix styling.
414470
*/

static/app/views/explore/logs/tables/logsInfiniteTable.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
55
import {ProjectFixture} from 'sentry-fixture/project';
66

77
import {
8+
act,
89
render,
910
screen,
1011
userEvent,
@@ -254,7 +255,9 @@ describe('LogsInfiniteTable', () => {
254255
for (const row of allTreeRows) {
255256
for (const field of visibleColumnFields) {
256257
await userEvent.hover(row, {delay: null});
257-
jest.advanceTimersByTime(DEFAULT_TRACE_ITEM_HOVER_TIMEOUT + 1);
258+
act(() => {
259+
jest.advanceTimersByTime(DEFAULT_TRACE_ITEM_HOVER_TIMEOUT + 1);
260+
});
258261
const cell = await within(row).findByTestId(`log-table-cell-${field}`);
259262
const actionsButton = within(cell).queryByRole('button', {
260263
name: 'Actions',

0 commit comments

Comments
 (0)