1
1
import type { CSSProperties , ReactNode } from 'react' ;
2
- import { isValidElement , useCallback } from 'react' ;
2
+ import { isValidElement , useCallback , useEffect , useRef } from 'react' ;
3
3
import styled from '@emotion/styled' ;
4
4
import beautify from 'js-beautify' ;
5
5
@@ -9,16 +9,19 @@ import {Button} from 'sentry/components/core/button';
9
9
import { Tooltip } from 'sentry/components/core/tooltip' ;
10
10
import ErrorBoundary from 'sentry/components/errorBoundary' ;
11
11
import Link from 'sentry/components/links/link' ;
12
+ import Placeholder from 'sentry/components/placeholder' ;
12
13
import { OpenReplayComparisonButton } from 'sentry/components/replays/breadcrumbs/openReplayComparisonButton' ;
13
14
import { useReplayContext } from 'sentry/components/replays/replayContext' ;
14
15
import { useReplayGroupContext } from 'sentry/components/replays/replayGroupContext' ;
15
16
import StructuredEventData from 'sentry/components/structuredEventData' ;
16
17
import { Timeline } from 'sentry/components/timeline' ;
17
18
import { t } from 'sentry/locale' ;
18
19
import { space } from 'sentry/styles/space' ;
20
+ import { trackAnalytics } from 'sentry/utils/analytics' ;
19
21
import type { Extraction } from 'sentry/utils/replays/extractDomNodes' ;
20
22
import { getReplayDiffOffsetsFromFrame } from 'sentry/utils/replays/getDiffTimestamps' ;
21
23
import getFrameDetails from 'sentry/utils/replays/getFrameDetails' ;
24
+ import useExtractDomNodes from 'sentry/utils/replays/hooks/useExtractDomNodes' ;
22
25
import type ReplayReader from 'sentry/utils/replays/replayReader' ;
23
26
import type {
24
27
ErrorFrame ,
@@ -45,41 +48,91 @@ import {makeFeedbackPathname} from 'sentry/views/userFeedback/pathnames';
45
48
type MouseCallback = ( frame : ReplayFrame , nodeId ?: number ) => void ;
46
49
47
50
interface Props {
51
+ allowShowSnippet : boolean ;
48
52
frame : ReplayFrame ;
49
53
onClick : null | MouseCallback ;
50
54
onInspectorExpanded : OnExpandCallback ;
51
55
onMouseEnter : MouseCallback ;
52
56
onMouseLeave : MouseCallback ;
57
+ onShowSnippet : ( ) => void ;
58
+ showSnippet : boolean ;
53
59
startTimestampMs : number ;
54
60
className ?: string ;
55
61
expandPaths ?: string [ ] ;
56
62
extraction ?: Extraction ;
57
63
ref ?: React . Ref < HTMLDivElement > ;
58
64
style ?: CSSProperties ;
65
+ updateDimensions ?: ( ) => void ;
59
66
}
60
67
61
68
function BreadcrumbItem ( {
62
69
className,
63
- extraction,
64
70
frame,
65
71
expandPaths,
66
72
onClick,
67
73
onInspectorExpanded,
68
74
onMouseEnter,
69
75
onMouseLeave,
76
+ showSnippet,
70
77
startTimestampMs,
71
78
style,
72
79
ref,
80
+ onShowSnippet,
81
+ updateDimensions,
82
+ allowShowSnippet,
73
83
} : Props ) {
74
84
const { color, description, title, icon} = getFrameDetails ( frame ) ;
75
85
const { replay} = useReplayContext ( ) ;
86
+ const organization = useOrganization ( ) ;
87
+ const { data : extraction , isPending} = useExtractDomNodes ( {
88
+ replay,
89
+ frame,
90
+ enabled : showSnippet ,
91
+ } ) ;
92
+
93
+ const prevExtractState = useRef ( isPending ) ;
94
+
95
+ useEffect ( ( ) => {
96
+ if ( ! updateDimensions ) {
97
+ return ;
98
+ }
99
+
100
+ if ( isPending !== prevExtractState . current || showSnippet ) {
101
+ prevExtractState . current = isPending ;
102
+ updateDimensions ( ) ;
103
+ }
104
+ } , [ isPending , updateDimensions , showSnippet ] ) ;
105
+
106
+ const handleViewHtml = useCallback (
107
+ ( e : React . MouseEvent < HTMLButtonElement > ) => {
108
+ onShowSnippet ( ) ;
109
+ e . preventDefault ( ) ;
110
+ e . stopPropagation ( ) ;
111
+ trackAnalytics ( 'replay.view_html' , {
112
+ organization,
113
+ breadcrumb_type : 'category' in frame ? frame . category : 'unknown' ,
114
+ } ) ;
115
+ } ,
116
+ [ onShowSnippet , organization , frame ]
117
+ ) ;
76
118
77
119
const renderDescription = useCallback ( ( ) => {
78
120
return typeof description === 'string' ||
79
121
( description !== undefined && isValidElement ( description ) ) ? (
80
- < Description title = { description } showOnlyOnOverflow isHoverable >
81
- { description }
82
- </ Description >
122
+ < DescriptionWrapper >
123
+ < Description title = { description } showOnlyOnOverflow isHoverable >
124
+ { description }
125
+ </ Description >
126
+
127
+ { allowShowSnippet &&
128
+ ! showSnippet &&
129
+ frame . data ?. nodeId !== undefined &&
130
+ ( ! isSpanFrame ( frame ) || ! isWebVitalFrame ( frame ) ) && (
131
+ < ViewHtmlButton priority = "link" onClick = { handleViewHtml } size = "xs" >
132
+ { t ( 'View HTML' ) }
133
+ </ ViewHtmlButton >
134
+ ) }
135
+ </ DescriptionWrapper >
83
136
) : (
84
137
< Wrapper >
85
138
< StructuredEventData
@@ -95,7 +148,15 @@ function BreadcrumbItem({
95
148
/>
96
149
</ Wrapper >
97
150
) ;
98
- } , [ description , expandPaths , onInspectorExpanded ] ) ;
151
+ } , [
152
+ description ,
153
+ expandPaths ,
154
+ frame ,
155
+ onInspectorExpanded ,
156
+ showSnippet ,
157
+ allowShowSnippet ,
158
+ handleViewHtml ,
159
+ ] ) ;
99
160
100
161
const renderComparisonButton = useCallback ( ( ) => {
101
162
return isBreadcrumbFrame ( frame ) && isHydrationErrorFrame ( frame ) && replay ? (
@@ -124,8 +185,14 @@ function BreadcrumbItem({
124
185
] ) ;
125
186
126
187
const renderCodeSnippet = useCallback ( ( ) => {
188
+ if ( showSnippet && isPending ) {
189
+ return < Placeholder height = "34px" /> ;
190
+ }
191
+
127
192
return (
128
193
( ! isSpanFrame ( frame ) || ! isWebVitalFrame ( frame ) ) &&
194
+ ! isPending &&
195
+ showSnippet &&
129
196
extraction ?. html ?. map ( html => (
130
197
< CodeContainer key = { html } >
131
198
< CodeSnippet language = "html" hideCopyButton >
@@ -134,7 +201,7 @@ function BreadcrumbItem({
134
201
</ CodeContainer >
135
202
) )
136
203
) ;
137
- } , [ extraction ?. html , frame ] ) ;
204
+ } , [ frame , isPending , extraction ?. html , showSnippet ] ) ;
138
205
139
206
const renderIssueLink = useCallback ( ( ) => {
140
207
return isErrorFrame ( frame ) || isFeedbackFrame ( frame ) ? (
@@ -350,6 +417,16 @@ const Description = styled(Tooltip)`
350
417
color: ${ p => p . theme . subText } ;
351
418
` ;
352
419
420
+ const DescriptionWrapper = styled ( 'div' ) `
421
+ display: flex;
422
+ gap: ${ space ( 1 ) } ;
423
+ justify-content: space-between;
424
+ ` ;
425
+
426
+ const ViewHtmlButton = styled ( Button ) `
427
+ white-space: nowrap;
428
+ ` ;
429
+
353
430
const StyledTimelineItem = styled ( Timeline . Item ) `
354
431
width: 100%;
355
432
position: relative;
0 commit comments