@@ -43,7 +43,28 @@ export const adjustTransactionDuration = (client: Client, span: Span, maxDuratio
4343 } ) ;
4444} ;
4545
46- export const ignoreEmptyBackNavigation = ( client : Client | undefined , span : Span | undefined ) : void => {
46+ /**
47+ * Helper function to filter out auto-instrumentation child spans.
48+ */
49+ function getMeaningfulChildSpans ( span : Span ) : Span [ ] {
50+ const children = getSpanDescendants ( span ) ;
51+ return children . filter (
52+ child =>
53+ child . spanContext ( ) . spanId !== span . spanContext ( ) . spanId &&
54+ spanToJSON ( child ) . op !== 'ui.load.initial_display' &&
55+ spanToJSON ( child ) . op !== 'navigation.processing' ,
56+ ) ;
57+ }
58+
59+ /**
60+ * Generic helper to discard empty navigation spans based on a condition.
61+ */
62+ function discardEmptyNavigationSpan (
63+ client : Client | undefined ,
64+ span : Span | undefined ,
65+ shouldDiscardFn : ( span : Span ) => boolean ,
66+ onDiscardFn : ( span : Span ) => void ,
67+ ) : void {
4768 if ( ! client ) {
4869 debug . warn ( 'Could not hook on spanEnd event because client is not defined.' ) ;
4970 return ;
@@ -55,7 +76,7 @@ export const ignoreEmptyBackNavigation = (client: Client | undefined, span: Span
5576 }
5677
5778 if ( ! isRootSpan ( span ) || ! isSentrySpan ( span ) ) {
58- debug . warn ( 'Not sampling empty back spans only works for Sentry Transactions (Root Spans).' ) ;
79+ debug . warn ( 'Not sampling empty navigation spans only works for Sentry Transactions (Root Spans).' ) ;
5980 return ;
6081 }
6182
@@ -64,27 +85,66 @@ export const ignoreEmptyBackNavigation = (client: Client | undefined, span: Span
6485 return ;
6586 }
6687
67- if ( ! spanToJSON ( span ) . data ?. [ 'route.has_been_seen' ] ) {
88+ if ( ! shouldDiscardFn ( span ) ) {
6889 return ;
6990 }
7091
71- const children = getSpanDescendants ( span ) ;
72- const filtered = children . filter (
73- child =>
74- child . spanContext ( ) . spanId !== span . spanContext ( ) . spanId &&
75- spanToJSON ( child ) . op !== 'ui.load.initial_display' &&
76- spanToJSON ( child ) . op !== 'navigation.processing' ,
77- ) ;
78-
79- if ( filtered . length <= 0 ) {
80- // filter children must include at least one span not created by the navigation automatic instrumentation
81- debug . log (
82- 'Not sampling transaction as route has been seen before. Pass ignoreEmptyBackNavigationTransactions = false to disable this feature.' ,
83- ) ;
84- // Route has been seen before and has no child spans.
92+ const meaningfulChildren = getMeaningfulChildSpans ( span ) ;
93+ if ( meaningfulChildren . length <= 0 ) {
94+ onDiscardFn ( span ) ;
8595 span [ '_sampled' ] = false ;
8696 }
8797 } ) ;
98+ }
99+
100+ export const ignoreEmptyBackNavigation = ( client : Client | undefined , span : Span | undefined ) : void => {
101+ discardEmptyNavigationSpan (
102+ client ,
103+ span ,
104+ // Only discard if route has been seen before
105+ span => spanToJSON ( span ) . data ?. [ 'route.has_been_seen' ] === true ,
106+ // Log message when discarding
107+ ( ) => {
108+ debug . log (
109+ 'Not sampling transaction as route has been seen before. Pass ignoreEmptyBackNavigationTransactions = false to disable this feature.' ,
110+ ) ;
111+ } ,
112+ ) ;
113+ } ;
114+
115+ /**
116+ * Discards empty "Route Change" transactions that never received route information.
117+ * This happens when navigation library emits a route change event but getCurrentRoute() returns undefined.
118+ * Such transactions don't contain any useful information and should not be sent to Sentry.
119+ *
120+ * This function must be called with a reference tracker function that can check if the span
121+ * was cleared from the integration's tracking (indicating it went through the state listener).
122+ */
123+ export const ignoreEmptyRouteChangeTransactions = (
124+ client : Client | undefined ,
125+ span : Span | undefined ,
126+ defaultNavigationSpanName : string ,
127+ isSpanStillTracked : ( ) => boolean ,
128+ ) : void => {
129+ discardEmptyNavigationSpan (
130+ client ,
131+ span ,
132+ // Only discard if:
133+ // 1. Still has default name
134+ // 2. No route information was set
135+ // 3. Still being tracked (state listener never called)
136+ span => {
137+ const spanJSON = spanToJSON ( span ) ;
138+ return (
139+ spanJSON . description === defaultNavigationSpanName && ! spanJSON . data ?. [ 'route.name' ] && isSpanStillTracked ( )
140+ ) ;
141+ } ,
142+ // Log and record dropped event
143+ _span => {
144+ debug . log ( `Discarding empty "${ defaultNavigationSpanName } " transaction that never received route information.` ) ;
145+ client ?. recordDroppedEvent ( 'sample_rate' , 'transaction' ) ;
146+ } ,
147+ ) ;
88148} ;
89149
90150/**
0 commit comments