Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,30 @@ describe('useTraceItemSearchQueryBuilderProps', () => {
});
});

it('uses a custom placeholder when provided', () => {
const {result} = renderHookWithProviders(useTraceItemSearchQueryBuilderProps, {
initialProps: {
...defaultInitialProps,
placeholder: 'Custom placeholder text',
},
organization,
});

expect(result.current.placeholder).toBe('Custom placeholder text');
});

it('falls back to the default placeholder for logs when no placeholder is provided', () => {
const {result} = renderHookWithProviders(useTraceItemSearchQueryBuilderProps, {
initialProps: {
...defaultInitialProps,
itemType: TraceItemDataset.LOGS,
},
organization,
});

expect(result.current.placeholder).toBe('Search for logs, users, tags, and more');
});

it('calls validateQuery when a new filter key is added', async () => {
const validateMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/trace-items/attributes/validate/',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ export function useTraceItemSearchQueryBuilderProps({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
placeholder,
}: TraceItemSearchQueryBuilderProps) {
const placeholderText = itemTypeToDefaultPlaceholder(itemType);
const placeholderText = placeholder ?? itemTypeToDefaultPlaceholder(itemType);

const {selection} = usePageFilters();
const effectiveProjects = projects ?? selection.projects;
Expand Down Expand Up @@ -259,6 +260,7 @@ export function TraceItemSearchQueryBuilder({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
placeholder,
}: TraceItemSearchQueryBuilderProps) {
const searchQueryBuilderProps = useTraceItemSearchQueryBuilderProps({
itemType,
Expand Down Expand Up @@ -289,6 +291,7 @@ export function TraceItemSearchQueryBuilder({
attributeQuery,
hiddenAttributeKeys,
allowedAttributeKeys,
placeholder,
});

return (
Expand Down
46 changes: 35 additions & 11 deletions static/app/views/explore/replays/detail/ourlogs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {defined} from 'sentry/utils';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
import {useReplayReader} from 'sentry/utils/replays/playback/providers/replayReaderProvider';
import {useCurrentHoverTime} from 'sentry/utils/replays/playback/providers/useCurrentHoverTime';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {defaultLogFields} from 'sentry/views/explore/contexts/logs/fields';
import {
LogsPageDataProvider,
Expand All @@ -23,11 +24,15 @@ import {
LogsInfiniteTable,
} from 'sentry/views/explore/logs/tables/logsInfiniteTable';
import {rearrangedLogsReplayFields} from 'sentry/views/explore/logs/tables/logsTableUtils';
import {useLogsSearchQueryBuilderProps} from 'sentry/views/explore/logs/useLogsSearchQueryBuilderProps';
import {
useQueryParamsSearch,
useSetQueryParamsSearch,
} from 'sentry/views/explore/queryParams/context';
import {FluidHeight} from 'sentry/views/explore/replays/detail/layout/fluidHeight';
import {NoRowRenderer} from 'sentry/views/explore/replays/detail/noRowRenderer';
import {OurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/ourlogFilters';
import {ourlogsAsFrames} from 'sentry/views/explore/replays/detail/ourlogs/ourlogsAsFrames';
import {useOurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/useOurLogFilters';

export function OurLogs() {
const replay = useReplayReader();
Expand Down Expand Up @@ -71,9 +76,12 @@ interface OurLogsContentProps {
}

function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {
const {attributes: stringAttributes} = useLogItemAttributes({}, 'string');
const {attributes: numberAttributes} = useLogItemAttributes({}, 'number');
const {attributes: booleanAttributes} = useLogItemAttributes({}, 'boolean');
const {attributes: stringAttributes, secondaryAliases: stringSecondaryAliases} =
useLogItemAttributes({}, 'string');
const {attributes: numberAttributes, secondaryAliases: numberSecondaryAliases} =
useLogItemAttributes({}, 'number');
const {attributes: booleanAttributes, secondaryAliases: booleanSecondaryAliases} =
useLogItemAttributes({}, 'boolean');

const {currentTime, setCurrentTime} = useReplayContext();
const [currentHoverTime] = useCurrentHoverTime();
Expand All @@ -82,9 +90,22 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {
const {infiniteLogsQueryResult} = useLogsPageData();
const {data: logItems, isPending} = infiniteLogsQueryResult;

const filterProps = useOurLogFilters({logItems});
const {items: filteredLogItems, setSearchTerm} = filterProps;
const clearSearchTerm = () => setSearchTerm('');
const logsSearch = useQueryParamsSearch();
const setLogsSearch = useSetQueryParamsSearch();
const filterText = logsSearch.freeText.join(' ');
const clearSearch = useCallback(
() => setLogsSearch(new MutableSearch('')),
[setLogsSearch]
);

const {tracesItemSearchQueryBuilderProps} = useLogsSearchQueryBuilderProps({
stringAttributes,
numberAttributes,
booleanAttributes,
stringSecondaryAliases,
numberSecondaryAliases,
booleanSecondaryAliases,
});

const handleReplayTimeClick = useCallback(
(offsetMs: string) => {
Expand Down Expand Up @@ -120,7 +141,10 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {

return (
<OurLogsContentWrapper>
<OurLogFilters logItems={logItems} replayId={replayId} {...filterProps} />
<OurLogFilters
replayId={replayId}
searchQueryBuilderProps={tracesItemSearchQueryBuilderProps}
/>
<LogsItemContainer border="primary" radius="md" flex="1 1 auto">
{isPending ? (
<Placeholder height="100%" />
Expand All @@ -134,12 +158,12 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) {
embeddedOptions={embeddedOptions}
expanded
localOnlyItemFilters={{
filteredItems: filteredLogItems,
filterText: filterProps.searchTerm,
filteredItems: logItems,
filterText,
}}
embeddedStyling={{disableBodyPadding: true, showVerticalScrollbar: false}}
emptyRenderer={() => (
<NoRowRenderer unfilteredItems={logItems} clearSearchTerm={clearSearchTerm}>
<NoRowRenderer unfilteredItems={logItems} clearSearchTerm={clearSearch}>
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
{t('No logs recorded')}
</NoRowRenderer>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen} from 'sentry-test/reactTestingLibrary';

import {PageFiltersStore} from 'sentry/components/pageFilters/store';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider';
import {OpenInLogsButton} from 'sentry/views/explore/replays/detail/ourlogs/openInLogsButton';

function Wrapper({children}: {children: React.ReactNode}) {
return (
<LogsQueryParamsProvider
analyticsPageSource={LogsAnalyticsPageSource.REPLAY_DETAILS}
source="state"
>
{children}
</LogsQueryParamsProvider>
);
}

describe('OpenInLogsButton', () => {
beforeEach(() => {
PageFiltersStore.init();
PageFiltersStore.onInitializeUrlState({
projects: [1],
environments: [],
datetime: {period: '14d', start: null, end: null, utc: false},
});
});

it('renders nothing when the explore feature flag is disabled', () => {
const organization = OrganizationFixture({features: []});

const {container} = render(
<Wrapper>
<OpenInLogsButton replayId="abc123" />
</Wrapper>,
{organization}
);

expect(container).toBeEmptyDOMElement();
});

it('renders the button when the explore feature flag is enabled', () => {
const organization = OrganizationFixture({
features: ['visibility-explore-view'],
});

render(
<Wrapper>
<OpenInLogsButton />
</Wrapper>,
{organization}
);

expect(screen.getByRole('button', {name: 'Open in Logs'})).toBeInTheDocument();
});

it('appends replay_id to the URL when replayId is provided', () => {
const organization = OrganizationFixture({
features: ['visibility-explore-view'],
});

render(
<Wrapper>
<OpenInLogsButton replayId="deadbeef" />
</Wrapper>,
{organization}
);

const link = screen.getByRole('button', {name: 'Open in Logs'});
expect(link).toHaveAttribute('href', expect.stringContaining('replay_id%3Adeadbeef'));
});

it('includes the existing search query before replay_id in the URL', () => {
const organization = OrganizationFixture({
features: ['visibility-explore-view'],
});

render(
<Wrapper>
<OpenInLogsButton replayId="deadbeef" />
</Wrapper>,
{organization}
);

const link = screen.getByRole('button', {name: 'Open in Logs'});
expect(link).toHaveAttribute(
'href',
expect.stringMatching(/logsQuery=.*replay_id%3Adeadbeef/)
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import {useOrganization} from 'sentry/utils/useOrganization';
import {getLogsUrl} from 'sentry/views/explore/logs/utils';
import {useQueryParamsSearch} from 'sentry/views/explore/queryParams/context';

type Props = {
searchTerm: string;
replayId?: string;
};

export function OpenInLogsButton({searchTerm, replayId}: Props) {
export function OpenInLogsButton({replayId}: Props) {
const organization = useOrganization();
const hasExploreEnabled = organization.features.includes('visibility-explore-view');
const {selection} = usePageFilters();
const logsSearch = useQueryParamsSearch();

if (!hasExploreEnabled) {
return null;
}

let query = searchTerm || '';
let query = logsSearch.formatString();
if (replayId) {
const existingQuery = query ? `${query} ` : '';
query = `${existingQuery}replay_id:${replayId}`;
Expand All @@ -32,7 +33,7 @@ export function OpenInLogsButton({searchTerm, replayId}: Props) {
});

return (
<LinkButton size="sm" to={url} openInNewTab>
<LinkButton size="md" to={url} openInNewTab>
{t('Open in Logs')}
</LinkButton>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';

import {PageFiltersStore} from 'sentry/components/pageFilters/store';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
import {FieldKind} from 'sentry/utils/fields';
import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider';
import {OurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/ourlogFilters';
import {TraceItemDataset} from 'sentry/views/explore/types';

const baseSearchQueryBuilderProps = {
itemType: TraceItemDataset.LOGS as TraceItemDataset.LOGS,
booleanAttributes: {},
booleanSecondaryAliases: {},
numberAttributes: {},
numberSecondaryAliases: {},
stringAttributes: {
'log.message': {key: 'log.message', name: 'log.message', kind: FieldKind.TAG},
},
stringSecondaryAliases: {},
initialQuery: '',
searchSource: 'ourlogs' as const,
onSearch: jest.fn(),
};

function Wrapper({children}: {children: React.ReactNode}) {
return (
<LogsQueryParamsProvider
analyticsPageSource={LogsAnalyticsPageSource.REPLAY_DETAILS}
source="state"
>
{children}
</LogsQueryParamsProvider>
);
}

describe('OurLogFilters', () => {
beforeEach(() => {
PageFiltersStore.init();
PageFiltersStore.onInitializeUrlState({
projects: [1],
environments: [],
datetime: {period: '14d', start: null, end: null, utc: false},
});
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: '/organizations/org-slug/trace-items/attributes/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/trace-items/attributes/validate/',
method: 'POST',
body: {attributes: {}},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/recent-searches/',
body: [],
});
});

it('renders with replay-specific placeholder text', async () => {
const organization = OrganizationFixture({features: ['visibility-explore-view']});

render(
<Wrapper>
<OurLogFilters searchQueryBuilderProps={baseSearchQueryBuilderProps} />
</Wrapper>,
{organization}
);

expect(
await screen.findByPlaceholderText('Search on log levels, messages, and more')
).toBeInTheDocument();
});

it('renders the Open in Logs button when explore feature is enabled', async () => {
const organization = OrganizationFixture({features: ['visibility-explore-view']});

render(
<Wrapper>
<OurLogFilters searchQueryBuilderProps={baseSearchQueryBuilderProps} />
</Wrapper>,
{organization}
);

expect(await screen.findByRole('button', {name: 'Open in Logs'})).toBeInTheDocument();
});

it('does not render the Open in Logs button when explore feature is disabled', async () => {
const organization = OrganizationFixture({features: []});

render(
<Wrapper>
<OurLogFilters searchQueryBuilderProps={baseSearchQueryBuilderProps} />
</Wrapper>,
{organization}
);

await waitFor(() =>
expect(screen.queryByRole('button', {name: 'Open in Logs'})).not.toBeInTheDocument()
);
});
});
Loading
Loading