@@ -1738,6 +1738,153 @@ func TestLeafSpanPruning_TraceStateGrouping_EmptyTraceState(t *testing.T) {
17381738 assert .Equal (t , int64 (3 ), spanCount .Int ())
17391739}
17401740
1741+ // TestParentKeyCollisionAcrossDepths verifies that when the same span name
1742+ // appears at multiple tree depths (causing buildParentGroupKey collisions),
1743+ // nodes from overwritten groups are not incorrectly removed. This is a
1744+ // regression test for a bug where the depth-2 parent group overwrites the
1745+ // depth-1 entry in aggregationGroups, leaving depth-1 nodes marked for
1746+ // removal but absent from the final plan.
1747+ //
1748+ // Trace structure (3 copies to meet MinSpansToAggregate=2 for parents):
1749+ //
1750+ // root
1751+ // ├── svc [depth-2 parent candidate]
1752+ // │ ├── svc [depth-1 parent candidate, SAME name as depth-2]
1753+ // │ │ ├── SELECT (leaf)
1754+ // │ │ └── SELECT (leaf)
1755+ // │ └── svc
1756+ // │ ├── SELECT (leaf)
1757+ // │ └── SELECT (leaf)
1758+ // ├── svc
1759+ // │ ├── svc
1760+ // │ │ ├── SELECT (leaf)
1761+ // │ │ └── SELECT (leaf)
1762+ // │ └── svc
1763+ // │ ├── SELECT (leaf)
1764+ // │ └── SELECT (leaf)
1765+ // └── svc
1766+ // ├── svc
1767+ // │ ├── SELECT (leaf)
1768+ // │ └── SELECT (leaf)
1769+ // └── svc
1770+ // ├── SELECT (leaf)
1771+ // └── SELECT (leaf)
1772+ //
1773+ // Expected result (22 spans → 4 spans):
1774+ //
1775+ // root
1776+ // ├── summary(svc, aggregates 3 outer svc spans)
1777+ // │ └── summary(svc, aggregates 6 inner svc spans)
1778+ // │ └── summary(SELECT, aggregates 12 leaf spans)
1779+ func TestParentKeyCollisionAcrossDepths (t * testing.T ) {
1780+ factory := NewFactory ()
1781+ cfg := factory .CreateDefaultConfig ().(* Config )
1782+ cfg .MinSpansToAggregate = 2
1783+ cfg .MaxParentDepth = - 1
1784+
1785+ tp , err := factory .CreateTraces (t .Context (), processortest .NewNopSettings (metadata .Type ), cfg , consumertest .NewNop ())
1786+ require .NoError (t , err )
1787+
1788+ td := createTestTraceWithParentKeyCollision (t )
1789+
1790+ // Before: 1 root + 3 outer-svc + 6 inner-svc + 12 SELECT = 22 spans
1791+ originalSpanCount := countSpans (td )
1792+ assert .Equal (t , 22 , originalSpanCount )
1793+
1794+ err = tp .ConsumeTraces (t .Context (), td )
1795+ require .NoError (t , err )
1796+
1797+ finalSpanCount := countSpans (td )
1798+
1799+ // Verify every non-summary span that remains is NOT an orphan: its parent
1800+ // must either be another span in the output or it must be the root.
1801+ remainingByID := make (map [pcommon.SpanID ]ptrace.Span )
1802+ rss := td .ResourceSpans ()
1803+ for i := 0 ; i < rss .Len (); i ++ {
1804+ ilss := rss .At (i ).ScopeSpans ()
1805+ for j := 0 ; j < ilss .Len (); j ++ {
1806+ spans := ilss .At (j ).Spans ()
1807+ for k := 0 ; k < spans .Len (); k ++ {
1808+ span := spans .At (k )
1809+ remainingByID [span .SpanID ()] = span
1810+ }
1811+ }
1812+ }
1813+ for id , span := range remainingByID {
1814+ parentID := span .ParentSpanID ()
1815+ if parentID .IsEmpty () {
1816+ continue // root span
1817+ }
1818+ _ , parentExists := remainingByID [parentID ]
1819+ assert .True (t , parentExists , "span %s (name=%s) has dangling parent %s — parent was removed but span was kept" ,
1820+ id , span .Name (), parentID )
1821+ }
1822+
1823+ // With the bug, depth-1 "svc" nodes (6 spans) get incorrectly removed,
1824+ // dropping the count too low. The expected result: root + 3 summaries.
1825+ t .Logf ("original=%d final=%d" , originalSpanCount , finalSpanCount )
1826+ assert .Equal (t , 4 , finalSpanCount ,
1827+ "expected root + 3 summary spans (SELECT, inner svc, outer svc)" )
1828+ }
1829+
1830+ func createTestTraceWithParentKeyCollision (t * testing.T ) ptrace.Traces {
1831+ t .Helper ()
1832+ td := ptrace .NewTraces ()
1833+ rs := td .ResourceSpans ().AppendEmpty ()
1834+ ss := rs .ScopeSpans ().AppendEmpty ()
1835+
1836+ traceID := pcommon .TraceID ([16 ]byte {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 })
1837+ rootSpanID := pcommon .SpanID ([8 ]byte {1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 })
1838+
1839+ root := ss .Spans ().AppendEmpty ()
1840+ root .SetTraceID (traceID )
1841+ root .SetSpanID (rootSpanID )
1842+ root .SetName ("root" )
1843+
1844+ spanIDCounter := byte (2 )
1845+ nextID := func () pcommon.SpanID {
1846+ id := pcommon .SpanID ([8 ]byte {spanIDCounter , 0 , 0 , 0 , 0 , 0 , 0 , 0 })
1847+ spanIDCounter ++
1848+ return id
1849+ }
1850+
1851+ // 3 outer "svc" spans (same name/kind/status → same parent group key)
1852+ for range 3 {
1853+ outerID := nextID ()
1854+ outer := ss .Spans ().AppendEmpty ()
1855+ outer .SetTraceID (traceID )
1856+ outer .SetSpanID (outerID )
1857+ outer .SetParentSpanID (rootSpanID )
1858+ outer .SetName ("svc" )
1859+ outer .Status ().SetCode (ptrace .StatusCodeOk )
1860+
1861+ // Each outer has 2 inner "svc" spans (same name → key collision with outer)
1862+ for range 2 {
1863+ innerID := nextID ()
1864+ inner := ss .Spans ().AppendEmpty ()
1865+ inner .SetTraceID (traceID )
1866+ inner .SetSpanID (innerID )
1867+ inner .SetParentSpanID (outerID )
1868+ inner .SetName ("svc" )
1869+ inner .Status ().SetCode (ptrace .StatusCodeOk )
1870+
1871+ // Each inner has 2 leaf SELECT spans
1872+ for range 2 {
1873+ leaf := ss .Spans ().AppendEmpty ()
1874+ leaf .SetTraceID (traceID )
1875+ leaf .SetSpanID (nextID ())
1876+ leaf .SetParentSpanID (innerID )
1877+ leaf .SetName ("SELECT" )
1878+ leaf .Status ().SetCode (ptrace .StatusCodeOk )
1879+ leaf .SetStartTimestamp (pcommon .Timestamp (1000000000 ))
1880+ leaf .SetEndTimestamp (pcommon .Timestamp (1000000100 ))
1881+ }
1882+ }
1883+ }
1884+
1885+ return td
1886+ }
1887+
17411888// Helper functions for TraceState tests
17421889
17431890func createTestTraceWithSameTraceState (t * testing.T , traceState string ) ptrace.Traces {
0 commit comments