Repository: vercel/next.js
Pull request: Tracing: Fix memory leak in span map

## What?

Fixes a small retainer object that is inserted on each request and not cleaned up.

While at it also swapped array -> Set for the type checks to leverage `has`​ instead of `includes`​.

Was highlighted by running the reproduction in #84648.

Changed files:
filename: packages/next/src/server/lib/trace/constants.ts
patch: @@ -125,7 +125,7 @@ type SpanTypes =
   | `${MiddlewareSpan}`
 
 // This list is used to filter out spans that are not relevant to the user
-export const NextVanillaSpanAllowlist = [
+export const NextVanillaSpanAllowlist = new Set([
   MiddlewareSpan.execute,
   BaseServerSpan.handleRequest,
   RenderSpan.getServerSideProps,
@@ -142,15 +142,15 @@ export const NextVanillaSpanAllowlist = [
   NextNodeServerSpan.getLayoutOrPageModule,
   NextNodeServerSpan.startResponse,
   NextNodeServerSpan.clientComponentLoading,
-]
+])
 
 // These Spans are allowed to be always logged
 // when the otel log prefix env is set
-export const LogSpanAllowList = [
+export const LogSpanAllowList = new Set([
   NextNodeServerSpan.findPageComponents,
   NextNodeServerSpan.createComponentTree,
   NextNodeServerSpan.clientComponentLoading,
-]
+])
 
 export {
   BaseServerSpan,

filename: packages/next/src/server/lib/trace/tracer.ts
patch: @@ -13,6 +13,8 @@ import type {
 } from 'next/dist/compiled/@opentelemetry/api'
 import { isThenable } from '../../../shared/lib/is-thenable'
 
+const NEXT_OTEL_PERFORMANCE_PREFIX = process.env.NEXT_OTEL_PERFORMANCE_PREFIX
+
 let api: typeof import('next/dist/compiled/@opentelemetry/api')
 
 // we want to allow users to use their own version of @opentelemetry/api if they
@@ -274,7 +276,7 @@ class NextTracerImpl implements NextTracer {
     const spanName = options.spanName ?? type
 
     if (
-      (!NextVanillaSpanAllowlist.includes(type) &&
+      (!NextVanillaSpanAllowlist.has(type) &&
         process.env.NEXT_OTEL_VERBOSE !== '1') ||
       options.hideSpan
     ) {
@@ -307,20 +309,26 @@ class NextTracerImpl implements NextTracer {
         spanName,
         options,
         (span: Span) => {
-          const startTime =
-            'performance' in globalThis && 'measure' in performance
-              ? globalThis.performance.now()
-              : undefined
+          let startTime: number | undefined
+          if (
+            NEXT_OTEL_PERFORMANCE_PREFIX &&
+            type &&
+            LogSpanAllowList.has(type)
+          ) {
+            startTime =
+              'performance' in globalThis && 'measure' in performance
+                ? globalThis.performance.now()
+                : undefined
+          }
 
+          let cleanedUp = false
           const onCleanup = () => {
+            if (cleanedUp) return
+            cleanedUp = true
             rootSpanAttributesStore.delete(spanId)
-            if (
-              startTime &&
-              process.env.NEXT_OTEL_PERFORMANCE_PREFIX &&
-              LogSpanAllowList.includes(type || ('' as any))
-            ) {
+            if (startTime) {
               performance.measure(
-                `${process.env.NEXT_OTEL_PERFORMANCE_PREFIX}:next-${(
+                `${NEXT_OTEL_PERFORMANCE_PREFIX}:next-${(
                   type.split('.').pop() || ''
                 ).replace(
                   /[A-Z]/g,
@@ -345,11 +353,18 @@ class NextTracerImpl implements NextTracer {
               )
             )
           }
-          try {
-            if (fn.length > 1) {
+          if (fn.length > 1) {
+            try {
               return fn(span, (err) => closeSpanWithError(span, err))
+            } catch (err: any) {
+              closeSpanWithError(span, err)
+              throw err
+            } finally {
+              onCleanup()
             }
+          }
 
+          try {
             const result = fn(span)
             if (isThenable(result)) {
               // If there's error make sure it throws
@@ -398,7 +413,7 @@ class NextTracerImpl implements NextTracer {
       args.length === 3 ? args : [args[0], {}, args[1]]
 
     if (
-      !NextVanillaSpanAllowlist.includes(name) &&
+      !NextVanillaSpanAllowlist.has(name) &&
       process.env.NEXT_OTEL_VERBOSE !== '1'
     ) {
       return fn