From 57328fea98cb1e33466390930c21c0324a0fb898 Mon Sep 17 00:00:00 2001 From: Kev Date: Fri, 8 Aug 2025 19:26:05 -0400 Subject: [PATCH 1/6] Add logs table row buttons (filters, copy as json etc.) This adds some actions below the details when opening a log in the logs table. --- .../explore/contexts/logs/logsPageParams.tsx | 22 ++++ static/app/views/explore/logs/styles.tsx | 26 +++- .../explore/logs/tables/logsTableRow.spec.tsx | 70 ++++++++++ .../explore/logs/tables/logsTableRow.tsx | 124 +++++++++++++++--- static/app/views/explore/logs/utils.tsx | 33 +++++ 5 files changed, 253 insertions(+), 22 deletions(-) diff --git a/static/app/views/explore/contexts/logs/logsPageParams.tsx b/static/app/views/explore/contexts/logs/logsPageParams.tsx index cec650641fcfb1..6bce2a64343e40 100644 --- a/static/app/views/explore/contexts/logs/logsPageParams.tsx +++ b/static/app/views/explore/contexts/logs/logsPageParams.tsx @@ -552,3 +552,25 @@ function getLogsParamsStorageKey(version: number) { function getPastLogsParamsStorageKey(version: number) { return `logs-params-v${version - 1}`; } + +export function useLogsAddSearchFilter() { + const setLogsSearch = useSetLogsSearch(); + const search = useLogsSearch(); + + return useCallback( + ({ + key, + value, + negated, + }: { + key: string; + value: string | number | boolean; + negated?: boolean; + }) => { + const newSearch = search.copy(); + newSearch.addFilterValue(`${negated ? '!' : ''}${key}`, String(value)); + setLogsSearch(newSearch); + }, + [setLogsSearch, search] + ); +} diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 5560d3c0c97675..10c2a396245186 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -45,7 +45,7 @@ export const LogTableRow = styled(TableRow)` export const LogAttributeTreeWrapper = styled('div')` padding: ${space(1)} ${space(1)}; - border-bottom: 1px solid ${p => p.theme.innerBorder}; + border-bottom: 0px; `; export const LogTableBodyCell = styled(TableBodyCell)` @@ -90,6 +90,30 @@ export const LogDetailTableBodyCell = styled(TableBodyCell)` padding: 0; } `; +export const LogDetailTableActionsCell = styled(TableBodyCell)` + padding-left: ${space(2)}; + padding-right: ${space(2)}; + padding-top: ${space(0.5)}; + padding-bottom: 0; + min-height: 0px; + + ${LogTableRow} & { + padding-left: ${space(2)}; + padding-right: ${space(2)}; + padding-top: ${space(0.5)}; + padding-bottom: 0; + } + &:last-child { + padding-left: ${space(2)}; + padding-right: ${space(2)}; + padding-top: ${space(0.5)}; + padding-bottom: 0; + } +`; +export const LogDetailTableActionsButtonBar = styled('div')` + display: flex; + gap: ${space(1)}; +`; export const DetailsWrapper = styled('tr')` align-items: center; diff --git a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx index 56fd1c5b871984..d17e6b25cc4176 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx @@ -380,4 +380,74 @@ describe('logsTableRow', () => { 'https://github.com/example/repo/blob/main/file.py' ); }); + + it('copies log as JSON when Copy as JSON button is clicked', async () => { + const mockWriteText = jest.fn().mockResolvedValue(undefined); + Object.defineProperty(window.navigator, 'clipboard', { + value: { + writeText: mockWriteText, + }, + writable: true, + }); + + render( + + + } + canDeferRenderElements={false} + /> + , + {organization, initialRouterConfig} + ); + + // Expand the row to show the action buttons + const logTableRow = await screen.findByTestId('log-table-row'); + await userEvent.click(logTableRow); + + await waitFor(() => { + expect(rowDetailsMock).toHaveBeenCalledTimes(1); + }); + + // Find and click the Copy as JSON button + const copyButton = await screen.findByRole('button', {name: 'Copy as JSON'}); + expect(copyButton).toBeInTheDocument(); + + await userEvent.click(copyButton); + + // Verify clipboard was called with JSON representation of the log + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledTimes(1); + }); + + const callArgs = mockWriteText.mock.calls[0]; + expect(callArgs).toBeDefined(); + expect(callArgs).toHaveLength(1); + + const copiedText = callArgs![0]; + expect(typeof copiedText).toBe('string'); + + // Verify it's valid JSON + expect(() => JSON.parse(copiedText)).not.toThrow(); + + // Verify it contains expected log data + const parsedData = JSON.parse(copiedText); + expect(parsedData).toMatchObject({ + message: 'test log body', + trace: '7b91699f', + severity: 'error', + item_id: '1', + }); + + // Verify the JSON structure matches what ourlogToJson produces + expect(parsedData).toHaveProperty('item_id', '1'); + expect(parsedData['tags[timestamp_precise,number]']).toBeDefined(); + expect(parsedData).not.toHaveProperty('sentry.item_id'); + }); }); diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx index e076f030ca2442..a9003c054782e2 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.tsx @@ -2,17 +2,21 @@ import type {ComponentProps, SyntheticEvent} from 'react'; import {Fragment, memo, useCallback, useLayoutEffect, useRef, useState} from 'react'; import {useTheme} from '@emotion/react'; +import {Button} from 'sentry/components/core/button'; import {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning'; import LoadingIndicator from 'sentry/components/loadingIndicator'; -import {IconWarning} from 'sentry/icons'; +import {IconAdd, IconJson, IconSpan, IconSubtract, IconWarning} from 'sentry/icons'; import {IconChevron} from 'sentry/icons/iconChevron'; import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; import type {EventsMetaType} from 'sentry/utils/discover/eventView'; import {FieldValueType} from 'sentry/utils/fields'; +import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import useProjectFromId from 'sentry/utils/useProjectFromId'; import CellAction, { @@ -28,10 +32,10 @@ import { useSetLogsAutoRefresh, } from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext'; import { + stripLogParamsFromLocation, + useLogsAddSearchFilter, useLogsAnalyticsPageSource, useLogsFields, - useLogsSearch, - useSetLogsSearch, } from 'sentry/views/explore/contexts/logs/logsPageParams'; import { DEFAULT_TRACE_ITEM_HOVER_TIMEOUT, @@ -51,6 +55,8 @@ import { DetailsWrapper, getLogColors, LogAttributeTreeWrapper, + LogDetailTableActionsButtonBar, + LogDetailTableActionsCell, LogDetailTableBodyCell, LogFirstCellContent, LogsTableBodyFirstCell, @@ -69,9 +75,13 @@ import { } from 'sentry/views/explore/logs/useLogsQuery'; import { adjustAliases, + adjustLogTraceID, getLogRowItem, getLogSeverityLevel, + ourlogToJson, } from 'sentry/views/explore/logs/utils'; +import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; +import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; type LogsRowProps = { dataRow: OurLogsResponseItem; @@ -124,8 +134,6 @@ export const LogRowContent = memo(function LogRowContent({ const location = useLocation(); const organization = useOrganization(); const fields = useLogsFields(); - const search = useLogsSearch(); - const setLogsSearch = useSetLogsSearch(); const autorefreshEnabled = useLogsAutoRefreshEnabled(); const setAutorefresh = useSetLogsAutoRefresh(); const measureRef = useRef(null); @@ -186,22 +194,7 @@ export const LogRowContent = memo(function LogRowContent({ } }, [isExpanded, onExpandHeight, dataRow]); - const addSearchFilter = useCallback( - ({ - key, - value, - negated, - }: { - key: string; - value: string | number | boolean; - negated?: boolean; - }) => { - const newSearch = search.copy(); - newSearch.addFilterValue(`${negated ? '!' : ''}${key}`, String(value)); - setLogsSearch(newSearch); - }, - [setLogsSearch, search] - ); + const addSearchFilter = useLogsAddSearchFilter(); const theme = useTheme(); const severityNumber = dataRow[OurLogKnownFieldKey.SEVERITY_NUMBER]; @@ -383,6 +376,13 @@ function LogRowDetails({ }) { const location = useLocation(); const organization = useOrganization(); + const navigate = useNavigate(); + const {onClick: betterCopyToClipboard} = useCopyToClipboard({ + text: ourlogToJson(dataRow), + successMessage: t('Copied!'), + errorMessage: t('Failed to copy'), + }); + const addSearchFilter = useLogsAddSearchFilter(); const project = useProjectFromId({ project_id: '' + dataRow[OurLogKnownFieldKey.PROJECT_ID], }); @@ -469,6 +469,88 @@ function LogRowDetails({ )} + {!isPending && data && ( + + + + + + + + + + + + )} ); } diff --git a/static/app/views/explore/logs/utils.tsx b/static/app/views/explore/logs/utils.tsx index e92f8dad3c47be..c806dde091086b 100644 --- a/static/app/views/explore/logs/utils.tsx +++ b/static/app/views/explore/logs/utils.tsx @@ -445,3 +445,36 @@ export function getLogsUrlFromSavedQueryUrl({ }, }); } + +export function ourlogToJson(ourlog: OurLogsResponseItem): string { + const copy = {...ourlog}; + let warned = false; + const warnOnce = (key: string) => { + if (!warned) { + warned = true; + warn( + fmt`Found sentry. prefix in ${key} while copying [project_id: ${ourlog.project_id}, user_email: ${ourlog.user_email}]` + ); + } + }; + // Trimming any sentry. prefixes + for (const key in copy) { + if (key.startsWith('sentry.')) { + const value = copy[key]; + if (value !== undefined) { + warnOnce(key); + delete copy[key]; + copy[key.replace('sentry.', '')] = value; + } + } + if (key.startsWith('tags[sentry.')) { + const value = copy[key]; + if (value !== undefined) { + warnOnce(key); + delete copy[key]; + copy[key.replace('tags[sentry.', 'tags[')] = value; + } + } + } + return JSON.stringify(copy, null, 2); +} From 254b151b4a7d174e921562a2a0712ec0bca29576 Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 11 Aug 2025 17:22:14 -0400 Subject: [PATCH 2/6] Fix font-weight --- static/app/views/explore/logs/styles.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 10c2a396245186..e22c996685dfd6 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -113,6 +113,9 @@ export const LogDetailTableActionsCell = styled(TableBodyCell)` export const LogDetailTableActionsButtonBar = styled('div')` display: flex; gap: ${space(1)}; + & button { + font-weight: ${p => p.theme.fontWeight.normal}; + } `; export const DetailsWrapper = styled('tr')` From 45bf302c1fcaf4645c678d4374888836bdb58882 Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 11 Aug 2025 17:25:25 -0400 Subject: [PATCH 3/6] Remove trace button for now --- .../explore/logs/tables/logsTableRow.tsx | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx index a9003c054782e2..f089b79363ed91 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.tsx @@ -5,7 +5,7 @@ import {useTheme} from '@emotion/react'; import {Button} from 'sentry/components/core/button'; import {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning'; import LoadingIndicator from 'sentry/components/loadingIndicator'; -import {IconAdd, IconJson, IconSpan, IconSubtract, IconWarning} from 'sentry/icons'; +import {IconAdd, IconJson, IconSubtract, IconWarning} from 'sentry/icons'; import {IconChevron} from 'sentry/icons/iconChevron'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -16,7 +16,6 @@ import type {EventsMetaType} from 'sentry/utils/discover/eventView'; import {FieldValueType} from 'sentry/utils/fields'; import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; import {useLocation} from 'sentry/utils/useLocation'; -import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import useProjectFromId from 'sentry/utils/useProjectFromId'; import CellAction, { @@ -32,7 +31,6 @@ import { useSetLogsAutoRefresh, } from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext'; import { - stripLogParamsFromLocation, useLogsAddSearchFilter, useLogsAnalyticsPageSource, useLogsFields, @@ -75,13 +73,10 @@ import { } from 'sentry/views/explore/logs/useLogsQuery'; import { adjustAliases, - adjustLogTraceID, getLogRowItem, getLogSeverityLevel, ourlogToJson, } from 'sentry/views/explore/logs/utils'; -import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; -import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; type LogsRowProps = { dataRow: OurLogsResponseItem; @@ -376,7 +371,6 @@ function LogRowDetails({ }) { const location = useLocation(); const organization = useOrganization(); - const navigate = useNavigate(); const {onClick: betterCopyToClipboard} = useCopyToClipboard({ text: ourlogToJson(dataRow), successMessage: t('Copied!'), @@ -522,32 +516,6 @@ function LogRowDetails({ {t('Copy as JSON')} - )} From ef87a9e739297d31ff1ca4cb06a4fe9c11c6e41b Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 11 Aug 2025 17:45:54 -0400 Subject: [PATCH 4/6] clean up padding, log details etc. --- static/app/views/explore/logs/constants.tsx | 4 +++ static/app/views/explore/logs/styles.tsx | 15 ++-------- .../explore/logs/tables/logsTableRow.tsx | 18 +++++++---- static/app/views/explore/logs/types.tsx | 1 + static/app/views/explore/logs/utils.tsx | 30 ++++++++++++++----- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/static/app/views/explore/logs/constants.tsx b/static/app/views/explore/logs/constants.tsx index b11c444c126d11..8142b7029f7f30 100644 --- a/static/app/views/explore/logs/constants.tsx +++ b/static/app/views/explore/logs/constants.tsx @@ -59,6 +59,10 @@ export const HiddenLogDetailFields: OurLogFieldKey[] = [ 'span_id', ]; +export const DeprecatedLogDetailFields: OurLogFieldKey[] = [ + OurLogKnownFieldKey.TIMESTAMP_NANOS, +]; + export const HiddenColumnEditorLogFields: OurLogFieldKey[] = [...AlwaysHiddenLogFields]; export const HiddenLogSearchFields: string[] = [...AlwaysHiddenLogFields]; diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index e22c996685dfd6..4307407ae9b2dd 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -91,23 +91,14 @@ export const LogDetailTableBodyCell = styled(TableBodyCell)` } `; export const LogDetailTableActionsCell = styled(TableBodyCell)` - padding-left: ${space(2)}; - padding-right: ${space(2)}; - padding-top: ${space(0.5)}; - padding-bottom: 0; + padding: ${space(0.5)} ${space(2)}; min-height: 0px; ${LogTableRow} & { - padding-left: ${space(2)}; - padding-right: ${space(2)}; - padding-top: ${space(0.5)}; - padding-bottom: 0; + padding: ${space(0.5)} ${space(2)}; } &:last-child { - padding-left: ${space(2)}; - padding-right: ${space(2)}; - padding-top: ${space(0.5)}; - padding-bottom: 0; + padding: ${space(0.5)} ${space(2)}; } `; export const LogDetailTableActionsButtonBar = styled('div')` diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx index f089b79363ed91..411b13d3390874 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.tsx @@ -371,11 +371,6 @@ function LogRowDetails({ }) { const location = useLocation(); const organization = useOrganization(); - const {onClick: betterCopyToClipboard} = useCopyToClipboard({ - text: ourlogToJson(dataRow), - successMessage: t('Copied!'), - errorMessage: t('Failed to copy'), - }); const addSearchFilter = useLogsAddSearchFilter(); const project = useProjectFromId({ project_id: '' + dataRow[OurLogKnownFieldKey.PROJECT_ID], @@ -398,6 +393,19 @@ function LogRowDetails({ enabled: !missingLogId, }); + const {onClick: betterCopyToClipboard} = useCopyToClipboard({ + text: ourlogToJson(data), + onCopy: () => { + trackAnalytics('logs.table.row_copied_as_json', { + log_id: String(dataRow[OurLogKnownFieldKey.ID]), + organization, + }); + }, + + successMessage: t('Copied!'), + errorMessage: t('Failed to copy'), + }); + const theme = useTheme(); const logColors = getLogColors(level, theme); const attributes = diff --git a/static/app/views/explore/logs/types.tsx b/static/app/views/explore/logs/types.tsx index 716588a6998ea5..5a4970dc8a413b 100644 --- a/static/app/views/explore/logs/types.tsx +++ b/static/app/views/explore/logs/types.tsx @@ -27,6 +27,7 @@ export enum OurLogKnownFieldKey { TIMESTAMP = 'timestamp', TIMESTAMP_PRECISE = 'tags[sentry.timestamp_precise,number]', OBSERVED_TIMESTAMP_PRECISE = 'sentry.observed_timestamp_nanos', + TIMESTAMP_NANOS = 'sentry.timestamp_nanos', // Deprecated CODE_FILE_PATH = 'code.file.path', CODE_LINE_NUMBER = 'tags[code.line.number,number]', CODE_FUNCTION_NAME = 'code.function.name', diff --git a/static/app/views/explore/logs/utils.tsx b/static/app/views/explore/logs/utils.tsx index c806dde091086b..9760bf03485089 100644 --- a/static/app/views/explore/logs/utils.tsx +++ b/static/app/views/explore/logs/utils.tsx @@ -31,8 +31,12 @@ import { import {LOGS_SORT_BYS_KEY} from 'sentry/views/explore/contexts/logs/sortBys'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; import {SavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; -import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails'; +import type { + TraceItemDetailsResponse, + TraceItemResponseAttribute, +} from 'sentry/views/explore/hooks/useTraceItemDetails'; import { + DeprecatedLogDetailFields, LogAttributesHumanLabel, LOGS_GRID_SCROLL_MIN_ITEM_THRESHOLD, } from 'sentry/views/explore/logs/constants'; @@ -446,23 +450,35 @@ export function getLogsUrlFromSavedQueryUrl({ }); } -export function ourlogToJson(ourlog: OurLogsResponseItem): string { - const copy = {...ourlog}; +export function ourlogToJson(ourlog: TraceItemDetailsResponse | undefined): string { + if (!ourlog) { + warn(fmt`cannot copy undefined ourlog`); + return ''; + } + + const copy: Record = { + ...ourlog.attributes.reduce((it, {name, value}) => ({...it, [name]: value}), {}), + id: ourlog.itemId, + }; let warned = false; - const warnOnce = (key: string) => { + const warnAttributeOnce = (key: string) => { if (!warned) { warned = true; warn( - fmt`Found sentry. prefix in ${key} while copying [project_id: ${ourlog.project_id}, user_email: ${ourlog.user_email}]` + fmt`Found sentry. prefix in ${key} while copying [project_id: ${copy.project_id ?? 'unknown'}, user_email: ${copy['user.email'] ?? 'unknown'}]` ); } }; + // Trimming any sentry. prefixes for (const key in copy) { + if (DeprecatedLogDetailFields.includes(key)) { + continue; + } if (key.startsWith('sentry.')) { const value = copy[key]; if (value !== undefined) { - warnOnce(key); + warnAttributeOnce(key); delete copy[key]; copy[key.replace('sentry.', '')] = value; } @@ -470,7 +486,7 @@ export function ourlogToJson(ourlog: OurLogsResponseItem): string { if (key.startsWith('tags[sentry.')) { const value = copy[key]; if (value !== undefined) { - warnOnce(key); + warnAttributeOnce(key); delete copy[key]; copy[key.replace('tags[sentry.', 'tags[')] = value; } From 550569f256b53c12efcc86f6fe065d1ee3fe7e95 Mon Sep 17 00:00:00 2001 From: Kev Date: Tue, 12 Aug 2025 19:29:00 -0400 Subject: [PATCH 5/6] Fix alias in test --- static/app/views/explore/logs/tables/logsTableRow.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx index 0a40d0424e21b2..7120e074351ffb 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx @@ -450,7 +450,7 @@ describe('logsTableRow', () => { // Verify the JSON structure matches what ourlogToJson produces expect(parsedData).toHaveProperty('item_id', '1'); - expect(parsedData['tags[timestamp_precise,number]']).toBeDefined(); + expect(parsedData[OurLogKnownFieldKey.TIMESTAMP_PRECISE]).toBeDefined(); expect(parsedData).not.toHaveProperty('sentry.item_id'); }); }); From 4d4897f49fdbcd1d4bb5c20066f8cb6ce15c87cf Mon Sep 17 00:00:00 2001 From: Kev Date: Wed, 13 Aug 2025 10:51:03 -0400 Subject: [PATCH 6/6] Add pending or error condition to the log conversion function --- static/app/views/explore/logs/tables/logsTableRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx index 090385231ec9eb..4779f9d46fc5f7 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.tsx @@ -391,7 +391,7 @@ function LogRowDetails({ }); const {onClick: betterCopyToClipboard} = useCopyToClipboard({ - text: ourlogToJson(data), + text: isPending || isError ? '' : ourlogToJson(data), onCopy: () => { trackAnalytics('logs.table.row_copied_as_json', { log_id: String(dataRow[OurLogKnownFieldKey.ID]),