@@ -179,76 +179,100 @@ export function formatEventOutput(
179179 event : Event ,
180180 options ?: {
181181 performanceTrace ?: Trace ;
182+ replaySummary ?: {
183+ apiService : SentryApiService ;
184+ organizationSlug : string ;
185+ relatedReplayIds ?: string [ ] ;
186+ } ;
182187 } ,
183188) {
184189 let output = "" ;
190+ const eventWithReplayMetadataStripped = options ?. replaySummary
191+ ? stripReplayMetadata ( event )
192+ : event ;
193+ const eventToRender = eventWithReplayMetadataStripped ;
194+
195+ if ( options ?. replaySummary ) {
196+ output += formatIssueReplayOutput ( {
197+ apiService : options . replaySummary . apiService ,
198+ organizationSlug : options . replaySummary . organizationSlug ,
199+ event,
200+ relatedReplayIds : options . replaySummary . relatedReplayIds ,
201+ } ) ;
202+ }
185203
186204 // Check if entries exist (may be undefined for unsupported event types)
187- if ( ! event . entries || ! Array . isArray ( event . entries ) ) {
205+ if ( ! eventToRender . entries || ! Array . isArray ( eventToRender . entries ) ) {
188206 // For unsupported event types, just show tags and contexts
189- output += formatTags ( event . tags ) ;
190- output += formatContext ( event . context ) ;
191- output += formatContexts ( event . contexts ) ;
207+ output += formatTags ( eventToRender . tags ) ;
208+ output += formatContext ( eventToRender . context ) ;
209+ output += formatContexts ( eventToRender . contexts ) ;
192210 return output ;
193211 }
194212
195213 // Look for the primary error information
196- const messageEntry = event . entries . find ( ( e ) => e . type === "message" ) ;
197- const exceptionEntry = event . entries . find ( ( e ) => e . type === "exception" ) ;
198- const threadsEntry = event . entries . find ( ( e ) => e . type === "threads" ) ;
199- const requestEntry = event . entries . find ( ( e ) => e . type === "request" ) ;
200- const spansEntry = event . entries . find ( ( e ) => e . type === "spans" ) ;
201- const cspEntry = event . entries . find ( ( e ) => e . type === "csp" ) ;
214+ const messageEntry = eventToRender . entries . find ( ( e ) => e . type === "message" ) ;
215+ const exceptionEntry = eventToRender . entries . find (
216+ ( e ) => e . type === "exception" ,
217+ ) ;
218+ const threadsEntry = eventToRender . entries . find ( ( e ) => e . type === "threads" ) ;
219+ const requestEntry = eventToRender . entries . find ( ( e ) => e . type === "request" ) ;
220+ const spansEntry = eventToRender . entries . find ( ( e ) => e . type === "spans" ) ;
221+ const cspEntry = eventToRender . entries . find ( ( e ) => e . type === "csp" ) ;
202222
203223 // Error message (if present)
204224 if ( messageEntry ) {
205225 output += formatMessageInterfaceOutput (
206- event ,
226+ eventToRender ,
207227 messageEntry . data as z . infer < typeof MessageEntrySchema > ,
208228 ) ;
209229 }
210230
211231 // Stack trace (from exception or threads)
212232 if ( exceptionEntry ) {
213233 output += formatExceptionInterfaceOutput (
214- event ,
234+ eventToRender ,
215235 exceptionEntry . data as z . infer < typeof ErrorEntrySchema > ,
216236 ) ;
217237 } else if ( threadsEntry ) {
218238 output += formatThreadsInterfaceOutput (
219- event ,
239+ eventToRender ,
220240 threadsEntry . data as z . infer < typeof ThreadsEntrySchema > ,
221241 ) ;
222242 }
223243
224244 // Request info (if HTTP error)
225245 if ( requestEntry ) {
226246 output += formatRequestInterfaceOutput (
227- event ,
247+ eventToRender ,
228248 requestEntry . data as z . infer < typeof RequestEntrySchema > ,
229249 ) ;
230250 }
231251
232252 // CSP violation details
233253 if ( cspEntry ) {
234- output += formatCspInterfaceOutput ( event , cspEntry . data ) ;
254+ output += formatCspInterfaceOutput ( eventToRender , cspEntry . data ) ;
235255 }
236256
237257 // Performance issue details (N+1 queries, etc.)
238258 // Pass spans data for additional context even if we have evidence
239- if ( event . type === "transaction" ) {
240- output += formatPerformanceIssueOutput ( event , spansEntry ?. data , options ) ;
259+ if ( eventToRender . type === "transaction" ) {
260+ output += formatPerformanceIssueOutput (
261+ eventToRender ,
262+ spansEntry ?. data ,
263+ options ,
264+ ) ;
241265 }
242266
243267 // Generic events (performance regressions, metric-based issues)
244268 // These have occurrence data with evidenceDisplay that needs formatting
245- if ( event . type === "generic" ) {
246- output += formatGenericEventOutput ( event ) ;
269+ if ( eventToRender . type === "generic" ) {
270+ output += formatGenericEventOutput ( eventToRender ) ;
247271 }
248272
249- output += formatTags ( event . tags ) ;
250- output += formatContext ( event . context ) ;
251- output += formatContexts ( event . contexts ) ;
273+ output += formatTags ( eventToRender . tags ) ;
274+ output += formatContext ( eventToRender . context ) ;
275+ output += formatContexts ( eventToRender . contexts ) ;
252276 return output ;
253277}
254278
@@ -1684,6 +1708,7 @@ export function formatIssueOutput({
16841708 autofixState,
16851709 performanceTrace,
16861710 externalIssues,
1711+ relatedReplayIds,
16871712 experimentalMode,
16881713} : {
16891714 organizationSlug : string ;
@@ -1693,6 +1718,7 @@ export function formatIssueOutput({
16931718 autofixState ?: AutofixRunState ;
16941719 performanceTrace ?: Trace ;
16951720 externalIssues ?: ExternalIssueList ;
1721+ relatedReplayIds ?: string [ ] ;
16961722 experimentalMode ?: boolean ;
16971723} ) {
16981724 let output = `# Issue ${ issue . shortId } in **${ organizationSlug } **\n\n` ;
@@ -1829,7 +1855,14 @@ export function formatIssueOutput({
18291855 output += `**Message**:\n${ event . message } \n` ;
18301856 }
18311857 output += "\n" ;
1832- output += formatEventOutput ( event , { performanceTrace } ) ;
1858+ output += formatEventOutput ( event , {
1859+ performanceTrace,
1860+ replaySummary : {
1861+ apiService,
1862+ organizationSlug,
1863+ relatedReplayIds,
1864+ } ,
1865+ } ) ;
18331866
18341867 // Add Seer context if available
18351868 if ( autofixState ) {
@@ -1855,3 +1888,156 @@ export function formatIssueOutput({
18551888 }
18561889 return output ;
18571890}
1891+
1892+ const MAX_DISPLAY_REPLAYS = 5 ;
1893+
1894+ function formatIssueReplayOutput ( {
1895+ apiService,
1896+ organizationSlug,
1897+ event,
1898+ relatedReplayIds,
1899+ } : {
1900+ apiService : SentryApiService ;
1901+ organizationSlug : string ;
1902+ event : Event ;
1903+ relatedReplayIds ?: string [ ] ;
1904+ } ) : string {
1905+ const attachedReplayId = getReplayIdFromEvent ( event ) ;
1906+ const normalizedRelatedReplayIds = dedupeReplayIds ( relatedReplayIds ?? [ ] ) ;
1907+ const additionalRelatedReplayIds = normalizedRelatedReplayIds . filter (
1908+ ( replayId ) => replayId !== attachedReplayId ,
1909+ ) ;
1910+
1911+ if ( ! attachedReplayId && normalizedRelatedReplayIds . length === 0 ) {
1912+ return "" ;
1913+ }
1914+
1915+ const lines : string [ ] = [ "## Session Replay" , "" ] ;
1916+
1917+ if ( attachedReplayId ) {
1918+ lines . push (
1919+ `**Attached Replay**: ${ apiService . getReplayUrl ( organizationSlug , attachedReplayId ) } ` ,
1920+ ) ;
1921+ }
1922+
1923+ if ( normalizedRelatedReplayIds . length > 0 ) {
1924+ lines . push (
1925+ `**Related Replay Count**: ${ normalizedRelatedReplayIds . length } ` ,
1926+ ) ;
1927+ }
1928+
1929+ if ( additionalRelatedReplayIds . length > 0 ) {
1930+ const displayedReplayIds = additionalRelatedReplayIds . slice (
1931+ 0 ,
1932+ MAX_DISPLAY_REPLAYS ,
1933+ ) ;
1934+
1935+ lines . push ( "" ) ;
1936+ lines . push (
1937+ attachedReplayId ? "### Other Related Replays" : "### Related Replays" ,
1938+ ) ;
1939+ lines . push ( "" ) ;
1940+
1941+ for ( const replayId of displayedReplayIds ) {
1942+ lines . push ( `- ${ apiService . getReplayUrl ( organizationSlug , replayId ) } ` ) ;
1943+ }
1944+
1945+ const remainingReplayCount =
1946+ additionalRelatedReplayIds . length - displayedReplayIds . length ;
1947+ if ( remainingReplayCount > 0 ) {
1948+ lines . push (
1949+ `- ... and ${ remainingReplayCount } more replay${ remainingReplayCount === 1 ? "" : "s" } ` ,
1950+ ) ;
1951+ }
1952+ }
1953+
1954+ const exampleReplayId =
1955+ attachedReplayId ?? normalizedRelatedReplayIds . at ( 0 ) ?? null ;
1956+ if ( exampleReplayId ) {
1957+ lines . push ( "" ) ;
1958+ lines . push (
1959+ `Use \`get_replay_details(organizationSlug='${ organizationSlug } ', replayId='${ exampleReplayId } ')\` to inspect a replay in detail.` ,
1960+ ) ;
1961+ }
1962+
1963+ return `${ lines . join ( "\n" ) } \n\n` ;
1964+ }
1965+
1966+ function getReplayIdFromEvent ( event : Event ) : string | null {
1967+ const replayContext = event . contexts ?. replay as
1968+ | Record < string , unknown >
1969+ | undefined ;
1970+ const replayContextId =
1971+ typeof replayContext ?. replay_id === "string"
1972+ ? replayContext . replay_id
1973+ : null ;
1974+ const replayTagId =
1975+ event . tags ?. find ( ( tag ) => tag . key === "replayId" || tag . key === "replay.id" )
1976+ ?. value ?? null ;
1977+
1978+ return normalizeReplayId ( replayContextId ?? replayTagId ) ;
1979+ }
1980+
1981+ function dedupeReplayIds ( replayIds : string [ ] ) : string [ ] {
1982+ const normalizedReplayIds : string [ ] = [ ] ;
1983+ const seenReplayIds = new Set < string > ( ) ;
1984+
1985+ for ( const replayId of replayIds ) {
1986+ const normalizedReplayId = normalizeReplayId ( replayId ) ;
1987+ if ( ! normalizedReplayId || seenReplayIds . has ( normalizedReplayId ) ) {
1988+ continue ;
1989+ }
1990+ seenReplayIds . add ( normalizedReplayId ) ;
1991+ normalizedReplayIds . push ( normalizedReplayId ) ;
1992+ }
1993+
1994+ return normalizedReplayIds ;
1995+ }
1996+
1997+ function normalizeReplayId ( replayId : string | null | undefined ) : string | null {
1998+ if ( typeof replayId !== "string" ) {
1999+ return null ;
2000+ }
2001+
2002+ const trimmedReplayId = replayId . trim ( ) ;
2003+ if ( ! trimmedReplayId ) {
2004+ return null ;
2005+ }
2006+
2007+ return trimmedReplayId . replace ( / - / g, "" ) ;
2008+ }
2009+
2010+ function stripReplayMetadata ( event : Event ) : Event {
2011+ const tags = event . tags ?. filter (
2012+ ( tag ) => tag . key !== "replay.id" && tag . key !== "replayId" ,
2013+ ) ;
2014+ const replayContext = event . contexts ?. replay ;
2015+
2016+ if ( ! replayContext ) {
2017+ return { ...event , tags } ;
2018+ }
2019+
2020+ const nextReplayContext = Object . fromEntries (
2021+ Object . entries ( replayContext ) . filter ( ( [ key ] ) => key !== "replay_id" ) ,
2022+ ) ;
2023+ const nonTypeReplayKeys = Object . keys ( nextReplayContext ) . filter (
2024+ ( key ) => key !== "type" ,
2025+ ) ;
2026+ const contexts =
2027+ nonTypeReplayKeys . length === 0
2028+ ? Object . fromEntries (
2029+ Object . entries ( event . contexts ?? { } ) . filter (
2030+ ( [ key ] ) => key !== "replay" ,
2031+ ) ,
2032+ )
2033+ : {
2034+ ...event . contexts ,
2035+ replay : nextReplayContext ,
2036+ } ;
2037+
2038+ return {
2039+ ...event ,
2040+ tags,
2041+ contexts,
2042+ } ;
2043+ }
0 commit comments