Repository: vercel/next.js
Pull request: test: reduce flakiness in opentelemetry test for prod mode

## Summary

Reduces flakiness in the OpenTelemetry e2e test (`test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts`) for production mode.

### Root Cause
In production mode, Next.js generates additional spans from:
- RSC prefetch requests (with `?_rsc` query param)
- Edge runtime boundary spans

These extra root spans cause `toMatchObject` assertions to fail because they receive more spans than expected.

### Changes

1. **Filter spans by expected `http.target`** in `expectTrace()`:
   - Extract expected `http.target` values from the match object
   - Filter root spans to only include those with matching targets
   - This excludes RSC prefetch and edge boundary spans that aren't part of the test assertion

2. **Fix TestExporter callback bug** in `instrumentation-test.ts`:
   - The success callback was called unconditionally (even after errors)
   - Added early `return` after error callback
   - Moved success callback inside the success path

## Test Plan
- [x] All 34 tests pass in production mode (`NEXT_TEST_MODE=start`)
- [ ] CI passes

Changed files:
filename: test/e2e/opentelemetry/instrumentation/instrumentation-test.ts
patch: @@ -71,13 +71,13 @@ class TestExporter implements SpanExporter {
           code: ExportResultCode.FAILED,
           error: new Error(`http status ${response.status}`),
         })
+        return
       }
+      resultCallback({ code: ExportResultCode.SUCCESS })
     } catch (e) {
-      console.warn('WARN: TestExporterP: error:', e)
+      console.warn('WARN: TestExporter: error:', e)
       resultCallback({ code: ExportResultCode.FAILED, error: e })
     }
-
-    resultCallback({ code: ExportResultCode.SUCCESS })
   }
   shutdown(): Promise<void> {
     return Promise.resolve()

filename: test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts
patch: @@ -1426,6 +1426,14 @@ async function expectTrace(
   match: SpanMatch[],
   edgeOnly?: boolean
 ) {
+  // Extract expected http.target values from the match to filter out extra spans
+  // that may be generated in production mode (e.g., RSC prefetch requests)
+  const expectedTargets = new Set(
+    match
+      .map((m) => m.attributes?.['http.target'] as string | undefined)
+      .filter(Boolean)
+  )
+
   await check(async () => {
     const traces = collector.getSpans()
 
@@ -1481,15 +1489,27 @@ async function expectTrace(
       })
     }
 
-    tree.sort((a, b) => {
+    // Filter root spans to only those matching expected http.target values
+    // This prevents flakiness from extra spans in prod mode (RSC prefetch, etc.)
+    const filteredTree =
+      expectedTargets.size > 0
+        ? tree.filter((span) => {
+            const target = span.attributes?.['http.target'] as
+              | string
+              | undefined
+            return target && expectedTargets.has(target)
+          })
+        : tree
+
+    filteredTree.sort((a, b) => {
       const runtimeDiff = (a.runtime ?? '').localeCompare(b.runtime ?? '')
       if (runtimeDiff !== 0) {
         return runtimeDiff
       }
       return a.name.localeCompare(b.name)
     })
 
-    expect(tree).toMatchObject(match)
+    expect(filteredTree).toMatchObject(match)
     return 'success'
   }, 'success')
 }