Skip to content

Commit 6634032

Browse files
k-fishlzhao-sentry
authored andcommitted
fix(ourlogs): Open log line in drawer (#98134)
### Summary This auto-expands the clicked log line when clicking from the issues section. Also in this PR is a style fix for the refetch spinners since embedded views don't have headers. <img width="786" height="175" alt="Screenshot 2025-08-22 at 1 28 18 PM" src="https://github.com/user-attachments/assets/a66ff7d1-11c4-415e-8ec0-54e55ae810cb" /> LOGS-224
1 parent 5d22d69 commit 6634032

File tree

6 files changed

+216
-47
lines changed

6 files changed

+216
-47
lines changed

static/app/components/events/ourlogs/ourlogsDrawer.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,17 @@ interface LogIssueDrawerProps {
3737
event: Event;
3838
group: Group;
3939
project: Project;
40+
embeddedOptions?: {
41+
openWithExpandedIds?: string[];
42+
};
4043
}
4144

42-
export function OurlogsDrawer({event, project, group}: LogIssueDrawerProps) {
45+
export function OurlogsDrawer({
46+
event,
47+
project,
48+
group,
49+
embeddedOptions,
50+
}: LogIssueDrawerProps) {
4351
const setLogsSearch = useSetLogsSearch();
4452
const logsSearch = useLogsSearch();
4553
const hasInfiniteFeature = useOrganization().features.includes(
@@ -90,7 +98,11 @@ export function OurlogsDrawer({event, project, group}: LogIssueDrawerProps) {
9098
<EventDrawerBody ref={containerRef}>
9199
<LogsTableContainer>
92100
{hasInfiniteFeature ? (
93-
<LogsInfiniteTable embedded scrollContainer={containerRef} />
101+
<LogsInfiniteTable
102+
embedded
103+
scrollContainer={containerRef}
104+
embeddedOptions={embeddedOptions}
105+
/>
94106
) : (
95107
<LogsTable allowPagination embedded />
96108
)}

static/app/components/events/ourlogs/ourlogsSection.spec.tsx

Lines changed: 136 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,60 @@
11
import {EventFixture} from 'sentry-fixture/event';
22
import {GroupFixture} from 'sentry-fixture/group';
3+
import {LogFixture} from 'sentry-fixture/log';
34
import {OrganizationFixture} from 'sentry-fixture/organization';
45
import {ProjectFixture} from 'sentry-fixture/project';
56

6-
import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
7+
import {
8+
render,
9+
screen,
10+
userEvent,
11+
waitFor,
12+
within,
13+
} from 'sentry-test/reactTestingLibrary';
714

815
import {OurlogsSection} from 'sentry/components/events/ourlogs/ourlogsSection';
16+
import PageFiltersStore from 'sentry/stores/pageFiltersStore';
17+
import ProjectsStore from 'sentry/stores/projectsStore';
918
import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
1019

1120
const TRACE_ID = '00000000000000000000000000000000';
1221

13-
const organization = OrganizationFixture({features: ['ourlogs-enabled']});
22+
jest.mock('@tanstack/react-virtual', () => {
23+
return {
24+
useWindowVirtualizer: jest.fn().mockReturnValue({
25+
getVirtualItems: jest.fn().mockReturnValue([
26+
{key: '1', index: 0, start: 0, end: 50, lane: 0},
27+
{key: '2', index: 1, start: 50, end: 100, lane: 0},
28+
{key: '3', index: 2, start: 100, end: 150, lane: 0},
29+
]),
30+
getTotalSize: jest.fn().mockReturnValue(150),
31+
options: {
32+
scrollMargin: 0,
33+
},
34+
scrollDirection: 'forward',
35+
scrollOffset: 0,
36+
isScrolling: false,
37+
}),
38+
useVirtualizer: jest.fn().mockReturnValue({
39+
getVirtualItems: jest.fn().mockReturnValue([
40+
{key: '1', index: 0, start: 0, end: 50, lane: 0},
41+
{key: '2', index: 1, start: 50, end: 100, lane: 0},
42+
{key: '3', index: 2, start: 100, end: 150, lane: 0},
43+
]),
44+
getTotalSize: jest.fn().mockReturnValue(150),
45+
options: {
46+
scrollMargin: 0,
47+
},
48+
scrollDirection: 'forward',
49+
scrollOffset: 0,
50+
isScrolling: false,
51+
}),
52+
};
53+
});
54+
55+
const organization = OrganizationFixture({
56+
features: ['ourlogs-enabled', 'ourlogs-infinite-scroll'],
57+
});
1458
const project = ProjectFixture();
1559
const group = GroupFixture();
1660
const event = EventFixture({
@@ -65,13 +109,69 @@ const event = EventFixture({
65109
});
66110

67111
describe('OurlogsSection', () => {
112+
let logId: string;
113+
let mockRequest: jest.Mock;
68114
beforeEach(() => {
69-
// the search query combobox is firing updates and causing console.errors
70-
jest.spyOn(console, 'error').mockImplementation(() => {});
115+
logId = '11111111111111111111111111111111';
116+
117+
ProjectsStore.loadInitialData([project]);
118+
119+
PageFiltersStore.init();
120+
PageFiltersStore.onInitializeUrlState(
121+
{
122+
projects: [parseInt(project.id, 10)],
123+
environments: [],
124+
datetime: {
125+
period: '14d',
126+
start: null,
127+
end: null,
128+
utc: null,
129+
},
130+
},
131+
new Set()
132+
);
133+
134+
MockApiClient.addMockResponse({
135+
url: `/projects/`,
136+
body: [project],
137+
});
138+
139+
MockApiClient.addMockResponse({
140+
url: `/projects/${organization.slug}/${project.slug}/`,
141+
body: project,
142+
});
143+
144+
mockRequest = MockApiClient.addMockResponse({
145+
url: `/organizations/${organization.slug}/trace-logs/`,
146+
body: {
147+
data: [
148+
LogFixture({
149+
[OurLogKnownFieldKey.ID]: logId,
150+
[OurLogKnownFieldKey.PROJECT_ID]: project.id,
151+
[OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
152+
[OurLogKnownFieldKey.TRACE_ID]: TRACE_ID,
153+
[OurLogKnownFieldKey.SEVERITY_NUMBER]: 0,
154+
[OurLogKnownFieldKey.MESSAGE]: 'i am a log',
155+
}),
156+
],
157+
meta: {},
158+
},
159+
});
160+
161+
MockApiClient.addMockResponse({
162+
url: `/organizations/${organization.slug}/trace-items/attributes/`,
163+
method: 'GET',
164+
body: {},
165+
});
166+
167+
MockApiClient.addMockResponse({
168+
url: '/organizations/org-slug/recent-searches/',
169+
body: [],
170+
});
71171
});
72172

73173
it('renders empty', () => {
74-
const mockRequest = MockApiClient.addMockResponse({
174+
const mockRequestEmpty = MockApiClient.addMockResponse({
75175
url: `/organizations/${organization.slug}/trace-logs/`,
76176
body: {
77177
data: [],
@@ -81,44 +181,40 @@ describe('OurlogsSection', () => {
81181
render(<OurlogsSection event={event} project={project} group={group} />, {
82182
organization,
83183
});
84-
expect(mockRequest).toHaveBeenCalledTimes(1);
184+
expect(mockRequestEmpty).toHaveBeenCalledTimes(1);
85185
expect(screen.queryByText(/Logs/)).not.toBeInTheDocument();
86186
});
87187

88188
it('renders logs', async () => {
89-
const now = new Date();
90-
const mockRequest = MockApiClient.addMockResponse({
91-
url: `/organizations/${organization.slug}/trace-logs/`,
189+
const mockRowDetailsRequest = MockApiClient.addMockResponse({
190+
url: `/projects/${organization.slug}/${project.slug}/trace-items/${logId}/`,
191+
method: 'GET',
92192
body: {
93-
data: [
94-
{
95-
'sentry.item_id': '11111111111111111111111111111111',
96-
'project.id': 1,
97-
trace: TRACE_ID,
98-
severity_number: 0,
99-
severity: 'info',
100-
timestamp: now.toISOString(),
101-
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: now.getTime() * 1e6,
102-
message: 'i am a log',
103-
},
193+
itemId: logId,
194+
timestamp: '2025-04-03T15:50:10+00:00',
195+
attributes: [
196+
{name: 'severity', type: 'str', value: 'error'},
197+
{name: 'special_field', type: 'str', value: 'special value'},
104198
],
105-
meta: {},
106199
},
107200
});
201+
108202
render(<OurlogsSection event={event} project={project} group={group} />, {
109203
organization,
204+
initialRouterConfig: {
205+
location: {
206+
pathname: `/organizations/${organization.slug}/issues/${group.id}/`,
207+
query: {
208+
project: project.id,
209+
},
210+
},
211+
},
110212
});
111213
expect(mockRequest).toHaveBeenCalledTimes(1);
112214

113-
// without waiting a few ticks, the test fails just before the
114-
// promise corresponding to the request resolves
115-
// by adding some ticks, it forces the test to wait a little longer
116-
// until the promise is resolved
117-
for (let i = 0; i < 10; i++) {
118-
await tick();
119-
}
120-
121-
expect(screen.getByText(/i am a log/)).toBeInTheDocument();
215+
await waitFor(() => {
216+
expect(screen.getByText(/i am a log/)).toBeInTheDocument();
217+
});
122218

123219
expect(
124220
screen.queryByRole('complementary', {name: 'logs drawer'})
@@ -129,6 +225,15 @@ describe('OurlogsSection', () => {
129225
const aside = screen.getByRole('complementary', {name: 'logs drawer'});
130226
expect(aside).toBeInTheDocument();
131227

132-
expect(within(aside).getByText(/i am a log/)).toBeInTheDocument();
228+
await waitFor(() => {
229+
expect(mockRowDetailsRequest).toHaveBeenCalledTimes(1);
230+
});
231+
232+
await waitFor(() => {
233+
expect(within(aside).getByText(/special value/)).toBeInTheDocument();
234+
});
235+
236+
expect(within(aside).getByTestId('tree-key-severity')).toBeInTheDocument();
237+
expect(within(aside).getByTestId('tree-key-severity')).toHaveTextContent('severity');
133238
});
134239
});

static/app/components/events/ourlogs/ourlogsSection.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function OurlogsSectionContent({
7272

7373
const limitToTraceId = event.contexts?.trace?.trace_id;
7474
const onOpenLogsDrawer = useCallback(
75-
(e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>) => {
75+
(e: React.MouseEvent, expandedLogId?: string) => {
7676
e.stopPropagation();
7777
trackAnalytics('logs.issue_details.drawer_opened', {
7878
organization,
@@ -87,7 +87,14 @@ function OurlogsSectionContent({
8787
>
8888
<LogsPageDataProvider>
8989
<TraceItemAttributeProvider traceItemType={TraceItemDataset.LOGS} enabled>
90-
<OurlogsDrawer group={group} event={event} project={project} />
90+
<OurlogsDrawer
91+
group={group}
92+
event={event}
93+
project={project}
94+
embeddedOptions={
95+
expandedLogId ? {openWithExpandedIds: [expandedLogId]} : undefined
96+
}
97+
/>
9198
</TraceItemAttributeProvider>
9299
</LogsPageDataProvider>
93100
</LogsPageParamsProvider>
@@ -106,6 +113,13 @@ function OurlogsSectionContent({
106113
},
107114
[group, event, project, openDrawer, organization, limitToTraceId]
108115
);
116+
117+
const onEmbeddedRowClick = useCallback(
118+
(logItemId: string, clickEvent: React.MouseEvent) => {
119+
onOpenLogsDrawer(clickEvent, logItemId);
120+
},
121+
[onOpenLogsDrawer]
122+
);
109123
if (!feature) {
110124
return null;
111125
}
@@ -125,7 +139,7 @@ function OurlogsSectionContent({
125139
title={t('Logs')}
126140
data-test-id="logs-data-section"
127141
>
128-
<SmallTableContentWrapper onClick={onOpenLogsDrawer}>
142+
<SmallTableContentWrapper>
129143
<SmallTable>
130144
<TableBody>
131145
{abbreviatedTableData?.map((row, index) => (
@@ -137,6 +151,7 @@ function OurlogsSectionContent({
137151
sharedHoverTimeoutRef={sharedHoverTimeoutRef}
138152
key={index}
139153
blockRowExpanding
154+
onEmbeddedRowClick={onEmbeddedRowClick}
140155
/>
141156
))}
142157
</TableBody>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,8 @@ export const FloatingBackToTopContainer = styled('div')<{
452452

453453
export const HoveringRowLoadingRendererContainer = styled('div')<{
454454
headerHeight: number;
455+
height: number;
455456
position: 'top' | 'bottom';
456-
rowHeight: number;
457457
}>`
458458
position: absolute;
459459
left: 0;
@@ -469,6 +469,6 @@ export const HoveringRowLoadingRendererContainer = styled('div')<{
469469
);
470470
align-items: center;
471471
justify-content: center;
472-
height: ${p => p.rowHeight * 3}px;
472+
height: ${p => p.height}px;
473473
${p => (p.position === 'top' ? 'top: 0px;' : 'bottom: 0px;')}
474474
`;

0 commit comments

Comments
 (0)