diff --git a/bench/instrument.js b/bench/instrument.js
deleted file mode 100644
index 38176de31cbe..000000000000
--- a/bench/instrument.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// Disable automatic instrumentation
-process.env.OTEL_NO_PATCH_MODULES = '*'
-
-const { NodeTracerProvider } = require('@opentelemetry/node')
-const { SimpleSpanProcessor } = require('@opentelemetry/tracing')
-const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin')
-
-const tracerProvider = new NodeTracerProvider({
-  // All automatic instrumentation plugins have to be disabled as it affects worker_thread/child_process bootup performance
-  plugins: {
-    mongodb: { enabled: false, path: '@opentelemetry/plugin-mongodb' },
-    grpc: { enabled: false, path: '@opentelemetry/plugin-grpc' },
-    '@grpc/grpc-js': { enabled: false, path: '@opentelemetry/plugin-grpc-js' },
-    http: { enabled: false, path: '@opentelemetry/plugin-http' },
-    https: { enabled: false, path: '@opentelemetry/plugin-https' },
-    mysql: { enabled: false, path: '@opentelemetry/plugin-mysql' },
-    pg: { enabled: false, path: '@opentelemetry/plugin-pg' },
-    redis: { enabled: false, path: '@opentelemetry/plugin-redis' },
-    ioredis: { enabled: false, path: '@opentelemetry/plugin-ioredis' },
-    'pg-pool': { enabled: false, path: '@opentelemetry/plugin-pg-pool' },
-    express: { enabled: false, path: '@opentelemetry/plugin-express' },
-    '@hapi/hapi': {
-      enabled: false,
-      path: '@opentelemetry/hapi-instrumentation',
-    },
-    koa: { enabled: false, path: '@opentelemetry/koa-instrumentation' },
-    dns: { enabled: false, path: '@opentelemetry/plugin-dns' },
-  },
-})
-
-tracerProvider.addSpanProcessor(
-  new SimpleSpanProcessor(
-    new ZipkinExporter({
-      serviceName: 'next-js',
-    })
-  )
-)
-
-tracerProvider.register()
diff --git a/package.json b/package.json
index 40bfcb93cfb7..09c3db1ce14c 100644
--- a/package.json
+++ b/package.json
@@ -34,8 +34,6 @@
     "publish-stable": "lerna version --force-publish",
     "lint-staged": "lint-staged",
     "next": "node --trace-deprecation packages/next/dist/bin/next",
-    "trace": "node --trace-deprecation -r ./bench/instrument.js packages/next/dist/bin/next",
-    "trace-debug": "node --inspect --trace-deprecation -r ./bench/instrument.js packages/next/dist/bin/next",
     "debug": "node --inspect packages/next/dist/bin/next"
   },
   "pre-commit": "lint-staged",
@@ -45,11 +43,6 @@
     "@babel/preset-react": "7.12.10",
     "@fullhuman/postcss-purgecss": "1.3.0",
     "@mdx-js/loader": "0.18.0",
-    "@opentelemetry/exporter-zipkin": "0.14.0",
-    "@opentelemetry/node": "0.14.0",
-    "@opentelemetry/plugin-http": "0.14.0",
-    "@opentelemetry/plugin-https": "0.14.0",
-    "@opentelemetry/tracing": "0.14.0",
     "@testing-library/react": "11.2.5",
     "@types/cheerio": "0.22.16",
     "@types/fs-extra": "8.1.0",
diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts
index bd84e06b5b81..f153fbbb9881 100755
--- a/packages/next/bin/next.ts
+++ b/packages/next/bin/next.ts
@@ -2,7 +2,6 @@
 import * as log from '../build/output/log'
 import arg from 'next/dist/compiled/arg/index.js'
 import { NON_STANDARD_NODE_ENV } from '../lib/constants'
-import opentelemetryApi from '@opentelemetry/api'
 ;['react', 'react-dom'].forEach((dependency) => {
   try {
     // When 'npm link' is used it checks the clone location. Not the project.
@@ -107,18 +106,7 @@ if (typeof React.Suspense === 'undefined') {
 process.on('SIGTERM', () => process.exit(0))
 process.on('SIGINT', () => process.exit(0))
 
-commands[command]()
-  .then((exec) => exec(forwardedArgs))
-  .then(async () => {
-    if (command === 'build') {
-      // @ts-ignore getDelegate exists
-      const tp = opentelemetryApi.trace.getTracerProvider().getDelegate()
-      if (tp.shutdown) {
-        await tp.shutdown()
-      }
-      process.exit(0)
-    }
-  })
+commands[command]().then((exec) => exec(forwardedArgs))
 
 if (command === 'dev') {
   const { CONFIG_FILE } = require('../next-server/lib/constants')
diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts
index 5957660eea40..0e57931bd042 100644
--- a/packages/next/build/index.ts
+++ b/packages/next/build/index.ts
@@ -70,7 +70,7 @@ import { generateBuildId } from './generate-build-id'
 import { isWriteable } from './is-writeable'
 import * as Log from './output/log'
 import createSpinner from './spinner'
-import { traceAsyncFn, traceFn, tracer } from './tracer'
+import { trace, setGlobal } from '../telemetry/trace'
 import {
   collectPages,
   detectConflictingPaths,
@@ -86,7 +86,6 @@ import {
 import getBaseWebpackConfig from './webpack-config'
 import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
 import { writeBuildId } from './write-build-id'
-import opentelemetryApi from '@opentelemetry/api'
 import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'
 
 const staticCheckWorker = require.resolve('./utils')
@@ -118,31 +117,26 @@ export default async function build(
   reactProductionProfiling = false,
   debugOutput = false
 ): Promise<void> {
-  const span = tracer.startSpan('next-build')
-  return traceAsyncFn(span, async () => {
+  const nextBuildSpan = trace('next-build')
+
+  return nextBuildSpan.traceAsyncFn(async () => {
     // attempt to load global env values so they are available in next.config.js
-    const { loadedEnvFiles } = traceFn(tracer.startSpan('load-dotenv'), () =>
-      loadEnvConfig(dir, false, Log)
-    )
+    const { loadedEnvFiles } = nextBuildSpan
+      .traceChild('load-dotenv')
+      .traceFn(() => loadEnvConfig(dir, false, Log))
 
-    const config = await traceAsyncFn(
-      tracer.startSpan('load-next-config'),
-      () => loadConfig(PHASE_PRODUCTION_BUILD, dir, conf)
-    )
+    const config = await nextBuildSpan
+      .traceChild('load-next-config')
+      .traceAsyncFn(() => loadConfig(PHASE_PRODUCTION_BUILD, dir, conf))
     const { target } = config
-    const buildId = await traceAsyncFn(
-      tracer.startSpan('generate-buildid'),
-      () => generateBuildId(config.generateBuildId, nanoid)
-    )
+    const buildId = await nextBuildSpan
+      .traceChild('generate-buildid')
+      .traceAsyncFn(() => generateBuildId(config.generateBuildId, nanoid))
     const distDir = path.join(dir, config.distDir)
 
-    const {
-      headers,
-      rewrites,
-      redirects,
-    } = await traceAsyncFn(tracer.startSpan('load-custom-routes'), () =>
-      loadCustomRoutes(config)
-    )
+    const { headers, rewrites, redirects } = await nextBuildSpan
+      .traceChild('load-custom-routes')
+      .traceAsyncFn(() => loadCustomRoutes(config))
 
     if (ciEnvironment.isCI && !ciEnvironment.hasNextSupport) {
       const cacheDir = path.join(distDir, 'cache')
@@ -162,6 +156,7 @@ export default async function build(
     })
 
     const telemetry = new Telemetry({ distDir })
+    setGlobal('telemetry', telemetry)
 
     const publicDir = path.join(dir, 'public')
     const pagesDir = findPagesDir(dir)
@@ -181,16 +176,17 @@ export default async function build(
     )
 
     const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors)
-    await traceAsyncFn(tracer.startSpan('verify-typescript-setup'), () =>
-      verifyTypeScriptSetup(dir, pagesDir, !ignoreTypeScriptErrors)
-    )
+    await nextBuildSpan
+      .traceChild('verify-typescript-setup')
+      .traceAsyncFn(() =>
+        verifyTypeScriptSetup(dir, pagesDir, !ignoreTypeScriptErrors)
+      )
 
     const isLikeServerless = isTargetLikeServerless(target)
 
-    const pagePaths: string[] = await traceAsyncFn(
-      tracer.startSpan('collect-pages'),
-      () => collectPages(pagesDir, config.pageExtensions)
-    )
+    const pagePaths: string[] = await nextBuildSpan
+      .traceChild('collect-pages')
+      .traceAsyncFn(() => collectPages(pagesDir, config.pageExtensions))
 
     // needed for static exporting since we want to replace with HTML
     // files
@@ -203,19 +199,21 @@ export default async function build(
       previewModeEncryptionKey: crypto.randomBytes(32).toString('hex'),
     }
 
-    const mappedPages = traceFn(tracer.startSpan('create-pages-mapping'), () =>
-      createPagesMapping(pagePaths, config.pageExtensions)
-    )
-    const entrypoints = traceFn(tracer.startSpan('create-entrypoints'), () =>
-      createEntrypoints(
-        mappedPages,
-        target,
-        buildId,
-        previewProps,
-        config,
-        loadedEnvFiles
+    const mappedPages = nextBuildSpan
+      .traceChild('create-pages-mapping')
+      .traceFn(() => createPagesMapping(pagePaths, config.pageExtensions))
+    const entrypoints = nextBuildSpan
+      .traceChild('create-entrypoints')
+      .traceFn(() =>
+        createEntrypoints(
+          mappedPages,
+          target,
+          buildId,
+          previewProps,
+          config,
+          loadedEnvFiles
+        )
       )
-    )
     const pageKeys = Object.keys(mappedPages)
     const conflictingPublicFiles: string[] = []
     const hasCustomErrorPage = mappedPages['/_error'].startsWith(
@@ -235,9 +233,9 @@ export default async function build(
       }
     }
 
-    await traceAsyncFn(
-      tracer.startSpan('public-dir-conflict-check'),
-      async () => {
+    await nextBuildSpan
+      .traceChild('public-dir-conflict-check')
+      .traceAsyncFn(async () => {
         // Check if pages conflict with files in `public`
         // Only a page of public file can be served, not both.
         for (const page in mappedPages) {
@@ -261,8 +259,7 @@ export default async function build(
             )}`
           )
         }
-      }
-    )
+      })
 
     const nestedReservedPages = pageKeys.filter((page) => {
       return (
@@ -339,13 +336,13 @@ export default async function build(
         defaultLocale: string
         localeDetection?: false
       }
-    } = traceFn(tracer.startSpan('generate-routes-manifest'), () => ({
+    } = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => ({
       version: 3,
       pages404: true,
       basePath: config.basePath,
-      redirects: redirects.map((r) => buildCustomRoute(r, 'redirect')),
-      rewrites: rewrites.map((r) => buildCustomRoute(r, 'rewrite')),
-      headers: headers.map((r) => buildCustomRoute(r, 'header')),
+      redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')),
+      rewrites: rewrites.map((r: any) => buildCustomRoute(r, 'rewrite')),
+      headers: headers.map((r: any) => buildCustomRoute(r, 'header')),
       dynamicRoutes: getSortedRoutes(pageKeys)
         .filter(isDynamicRoute)
         .map((page) => {
@@ -361,9 +358,9 @@ export default async function build(
       i18n: config.i18n || undefined,
     }))
 
-    const distDirCreated = await traceAsyncFn(
-      tracer.startSpan('create-distdir'),
-      async () => {
+    const distDirCreated = await nextBuildSpan
+      .traceChild('create-dist-dir')
+      .traceAsyncFn(async () => {
         try {
           await promises.mkdir(distDir, { recursive: true })
           return true
@@ -373,8 +370,7 @@ export default async function build(
           }
           throw err
         }
-      }
-    )
+      })
 
     if (!distDirCreated || !(await isWriteable(distDir))) {
       throw new Error(
@@ -384,13 +380,15 @@ export default async function build(
 
     // We need to write the manifest with rewrites before build
     // so serverless can import the manifest
-    await traceAsyncFn(tracer.startSpan('write-routes-manifest'), () =>
-      promises.writeFile(
-        routesManifestPath,
-        JSON.stringify(routesManifest),
-        'utf8'
+    await nextBuildSpan
+      .traceChild('write-routes-manifest')
+      .traceAsyncFn(() =>
+        promises.writeFile(
+          routesManifestPath,
+          JSON.stringify(routesManifest),
+          'utf8'
+        )
       )
-    )
 
     const manifestPath = path.join(
       distDir,
@@ -398,9 +396,9 @@ export default async function build(
       PAGES_MANIFEST
     )
 
-    const requiredServerFiles = traceFn(
-      tracer.startSpan('generate-required-server-files'),
-      () => ({
+    const requiredServerFiles = nextBuildSpan
+      .traceChild('generate-required-server-files')
+      .traceFn(() => ({
         version: 1,
         config: {
           ...config,
@@ -425,12 +423,11 @@ export default async function build(
           .filter(nonNullable)
           .map((file) => path.join(config.distDir, file)),
         ignore: [] as string[],
-      })
-    )
+      }))
 
-    const configs = await traceAsyncFn(
-      tracer.startSpan('generate-webpack-config'),
-      () =>
+    const configs = await nextBuildSpan
+      .traceChild('generate-webpack-config')
+      .traceAsyncFn(() =>
         Promise.all([
           getBaseWebpackConfig(dir, {
             buildId,
@@ -453,7 +450,7 @@ export default async function build(
             rewrites,
           }),
         ])
-    )
+      )
 
     const clientConfig = configs[0]
 
@@ -488,10 +485,9 @@ export default async function build(
         }
       }
     } else {
-      result = await traceAsyncFn(
-        tracer.startSpan('run-webpack-compiler'),
-        () => runCompiler(configs)
-      )
+      result = await nextBuildSpan
+        .traceChild('run-webpack-compiler')
+        .traceAsyncFn(() => runCompiler(configs))
     }
 
     const webpackBuildEnd = process.hrtime(webpackBuildStart)
@@ -499,9 +495,9 @@ export default async function build(
       buildSpinner.stopAndPersist()
     }
 
-    result = traceFn(tracer.startSpan('format-webpack-messages'), () =>
-      formatWebpackMessages(result)
-    )
+    result = nextBuildSpan
+      .traceChild('format-webpack-messages')
+      .traceFn(() => formatWebpackMessages(result))
 
     if (result.errors.length > 0) {
       // Only keep the first error. Others are often indicative
@@ -582,9 +578,8 @@ export default async function build(
     const analysisBegin = process.hrtime()
     let hasSsrAmpPages = false
 
-    const staticCheckSpan = tracer.startSpan('static-check')
-    const { hasNonStaticErrorPage } = await traceAsyncFn(
-      staticCheckSpan,
+    const staticCheckSpan = nextBuildSpan.traceChild('static-check')
+    const { hasNonStaticErrorPage } = await staticCheckSpan.traceAsyncFn(
       async () => {
         process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD
 
@@ -601,8 +596,10 @@ export default async function build(
           serverRuntimeConfig: config.serverRuntimeConfig,
         }
 
-        const nonStaticErrorPage = await traceAsyncFn(
-          tracer.startSpan('check-static-error-page'),
+        const nonStaticErrorPageSpan = staticCheckSpan.traceChild(
+          'check-static-error-page'
+        )
+        const nonStaticErrorPage = await nonStaticErrorPageSpan.traceAsyncFn(
           async () =>
             hasCustomErrorPage &&
             (await hasCustomGetInitialProps(
@@ -646,143 +643,134 @@ export default async function build(
 
         await Promise.all(
           pageKeys.map(async (page) => {
-            return traceAsyncFn(
-              tracer.startSpan('check-page', { attributes: { page } }),
-              async () => {
-                const actualPage = normalizePagePath(page)
-                const [selfSize, allSize] = await getJsPageSizeInKb(
-                  actualPage,
-                  distDir,
-                  buildManifest
-                )
+            const checkPageSpan = staticCheckSpan.traceChild('check-page', {
+              page,
+            })
+            return checkPageSpan.traceAsyncFn(async () => {
+              const actualPage = normalizePagePath(page)
+              const [selfSize, allSize] = await getJsPageSizeInKb(
+                actualPage,
+                distDir,
+                buildManifest
+              )
 
-                let isSsg = false
-                let isStatic = false
-                let isHybridAmp = false
-                let ssgPageRoutes: string[] | null = null
+              let isSsg = false
+              let isStatic = false
+              let isHybridAmp = false
+              let ssgPageRoutes: string[] | null = null
 
-                const nonReservedPage = !page.match(
-                  /^\/(_app|_error|_document|api(\/|$))/
-                )
+              const nonReservedPage = !page.match(
+                /^\/(_app|_error|_document|api(\/|$))/
+              )
 
-                if (nonReservedPage) {
-                  try {
-                    let workerResult = await traceAsyncFn(
-                      tracer.startSpan('is-page-static'),
-                      () => {
-                        const spanContext = {}
-
-                        opentelemetryApi.propagation.inject(
-                          opentelemetryApi.context.active(),
-                          spanContext
-                        )
-                        return staticCheckWorkers.isPageStatic(
-                          page,
-                          distDir,
-                          isLikeServerless,
-                          runtimeEnvConfig,
-                          config.i18n?.locales,
-                          config.i18n?.defaultLocale,
-                          spanContext
-                        )
-                      }
+              if (nonReservedPage) {
+                try {
+                  let isPageStaticSpan = checkPageSpan.traceChild(
+                    'is-page-static'
+                  )
+                  let workerResult = await isPageStaticSpan.traceAsyncFn(() => {
+                    return staticCheckWorkers.isPageStatic(
+                      page,
+                      distDir,
+                      isLikeServerless,
+                      runtimeEnvConfig,
+                      config.i18n?.locales,
+                      config.i18n?.defaultLocale,
+                      isPageStaticSpan.id
                     )
+                  })
 
-                    if (
-                      workerResult.isStatic === false &&
-                      (workerResult.isHybridAmp || workerResult.isAmpOnly)
-                    ) {
-                      hasSsrAmpPages = true
-                    }
+                  if (
+                    workerResult.isStatic === false &&
+                    (workerResult.isHybridAmp || workerResult.isAmpOnly)
+                  ) {
+                    hasSsrAmpPages = true
+                  }
 
-                    if (workerResult.isHybridAmp) {
-                      isHybridAmp = true
-                      hybridAmpPages.add(page)
-                    }
+                  if (workerResult.isHybridAmp) {
+                    isHybridAmp = true
+                    hybridAmpPages.add(page)
+                  }
 
-                    if (workerResult.isNextImageImported) {
-                      isNextImageImported = true
-                    }
+                  if (workerResult.isNextImageImported) {
+                    isNextImageImported = true
+                  }
 
-                    if (workerResult.hasStaticProps) {
-                      ssgPages.add(page)
-                      isSsg = true
+                  if (workerResult.hasStaticProps) {
+                    ssgPages.add(page)
+                    isSsg = true
 
-                      if (
-                        workerResult.prerenderRoutes &&
-                        workerResult.encodedPrerenderRoutes
-                      ) {
-                        additionalSsgPaths.set(
-                          page,
-                          workerResult.prerenderRoutes
-                        )
-                        additionalSsgPathsEncoded.set(
-                          page,
-                          workerResult.encodedPrerenderRoutes
-                        )
-                        ssgPageRoutes = workerResult.prerenderRoutes
-                      }
-
-                      if (workerResult.prerenderFallback === 'blocking') {
-                        ssgBlockingFallbackPages.add(page)
-                      } else if (workerResult.prerenderFallback === true) {
-                        ssgStaticFallbackPages.add(page)
-                      }
-                    } else if (workerResult.hasServerProps) {
-                      serverPropsPages.add(page)
-                    } else if (
-                      workerResult.isStatic &&
-                      customAppGetInitialProps === false
+                    if (
+                      workerResult.prerenderRoutes &&
+                      workerResult.encodedPrerenderRoutes
                     ) {
-                      staticPages.add(page)
-                      isStatic = true
+                      additionalSsgPaths.set(page, workerResult.prerenderRoutes)
+                      additionalSsgPathsEncoded.set(
+                        page,
+                        workerResult.encodedPrerenderRoutes
+                      )
+                      ssgPageRoutes = workerResult.prerenderRoutes
                     }
 
-                    if (hasPages404 && page === '/404') {
-                      if (
-                        !workerResult.isStatic &&
-                        !workerResult.hasStaticProps
-                      ) {
-                        throw new Error(
-                          `\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
-                        )
-                      }
-                      // we need to ensure the 404 lambda is present since we use
-                      // it when _app has getInitialProps
-                      if (
-                        customAppGetInitialProps &&
-                        !workerResult.hasStaticProps
-                      ) {
-                        staticPages.delete(page)
-                      }
+                    if (workerResult.prerenderFallback === 'blocking') {
+                      ssgBlockingFallbackPages.add(page)
+                    } else if (workerResult.prerenderFallback === true) {
+                      ssgStaticFallbackPages.add(page)
                     }
+                  } else if (workerResult.hasServerProps) {
+                    serverPropsPages.add(page)
+                  } else if (
+                    workerResult.isStatic &&
+                    customAppGetInitialProps === false
+                  ) {
+                    staticPages.add(page)
+                    isStatic = true
+                  }
 
+                  if (hasPages404 && page === '/404') {
                     if (
-                      STATIC_STATUS_PAGES.includes(page) &&
                       !workerResult.isStatic &&
                       !workerResult.hasStaticProps
                     ) {
                       throw new Error(
-                        `\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
+                        `\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
                       )
                     }
-                  } catch (err) {
-                    if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err
-                    invalidPages.add(page)
+                    // we need to ensure the 404 lambda is present since we use
+                    // it when _app has getInitialProps
+                    if (
+                      customAppGetInitialProps &&
+                      !workerResult.hasStaticProps
+                    ) {
+                      staticPages.delete(page)
+                    }
                   }
-                }
 
-                pageInfos.set(page, {
-                  size: selfSize,
-                  totalSize: allSize,
-                  static: isStatic,
-                  isSsg,
-                  isHybridAmp,
-                  ssgPageRoutes,
-                  initialRevalidateSeconds: false,
-                })
+                  if (
+                    STATIC_STATUS_PAGES.includes(page) &&
+                    !workerResult.isStatic &&
+                    !workerResult.hasStaticProps
+                  ) {
+                    throw new Error(
+                      `\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
+                    )
+                  }
+                } catch (err) {
+                  if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err
+                  invalidPages.add(page)
+                }
               }
-            )
+
+              pageInfos.set(page, {
+                size: selfSize,
+                totalSize: allSize,
+                static: isStatic,
+                isSsg,
+                isHybridAmp,
+                ssgPageRoutes,
+                initialRevalidateSeconds: false,
+              })
+            })
           })
         )
         staticCheckWorkers.end()
@@ -917,7 +905,8 @@ export default async function build(
     const hasPages500 = usedStaticStatusPages.includes('/500')
     const useDefaultStatic500 = !hasPages500 && !hasNonStaticErrorPage
 
-    await traceAsyncFn(tracer.startSpan('static-generation'), async () => {
+    const staticGenerationSpan = nextBuildSpan.traceChild('static-generation')
+    await staticGenerationSpan.traceAsyncFn(async () => {
       const combinedPages = [...staticPages, ...ssgPages]
 
       detectConflictingPaths(
@@ -1061,9 +1050,9 @@ export default async function build(
         ext: 'html' | 'json',
         additionalSsgFile = false
       ) => {
-        return traceAsyncFn(
-          tracer.startSpan('move-exported-page'),
-          async () => {
+        return staticGenerationSpan
+          .traceChild('move-exported-page')
+          .traceAsyncFn(async () => {
             file = `${file}.${ext}`
             const orig = path.join(exportOptions.outdir, file)
             const pagePath = getPagePath(originPage, distDir, isLikeServerless)
@@ -1163,8 +1152,7 @@ export default async function build(
                 await promises.rename(updatedOrig, updatedDest)
               }
             }
-          }
-        )
+          })
       }
 
       // Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page
@@ -1400,7 +1388,6 @@ export default async function build(
       }),
       'utf8'
     )
-
     await promises.writeFile(
       path.join(distDir, EXPORT_MARKER),
       JSON.stringify({
@@ -1423,7 +1410,7 @@ export default async function build(
       allPageInfos.set(key, info)
     })
 
-    await traceAsyncFn(tracer.startSpan('print-tree-view'), () =>
+    await nextBuildSpan.traceChild('print-tree-view').traceAsyncFn(() =>
       printTreeView(Object.keys(mappedPages), allPageInfos, isLikeServerless, {
         distPath: distDir,
         buildId: buildId,
@@ -1435,9 +1422,9 @@ export default async function build(
     )
 
     if (debugOutput) {
-      traceFn(tracer.startSpan('print-custom-routes'), () =>
-        printCustomRoutes({ redirects, rewrites, headers })
-      )
+      nextBuildSpan
+        .traceChild('print-custom-routes')
+        .traceFn(() => printCustomRoutes({ redirects, rewrites, headers }))
     }
 
     if (config.analyticsId) {
@@ -1449,9 +1436,9 @@ export default async function build(
       console.log('')
     }
 
-    await traceAsyncFn(tracer.startSpan('telemetry-flush'), () =>
-      telemetry.flush()
-    )
+    await nextBuildSpan
+      .traceChild('telemetry-flush')
+      .traceAsyncFn(() => telemetry.flush())
   })
 }
 
diff --git a/packages/next/build/tracer.ts b/packages/next/build/tracer.ts
deleted file mode 100644
index e1da864eb6f0..000000000000
--- a/packages/next/build/tracer.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import api, { Span } from '@opentelemetry/api'
-
-export const tracer = api.trace.getTracer('next', process.env.__NEXT_VERSION)
-
-const compilerStacks = new WeakMap()
-const compilerStoppedSpans = new WeakMap()
-
-export function stackPush(compiler: any, spanName: string, attrs?: any): any {
-  let stack = compilerStacks.get(compiler)
-  let span
-
-  if (!stack) {
-    compilerStacks.set(compiler, (stack = []))
-    span = tracer.startSpan(spanName, attrs ? attrs() : undefined)
-  } else {
-    const parent = stack[stack.length - 1]
-    if (parent) {
-      tracer.withSpan(parent, () => {
-        span = tracer.startSpan(spanName, attrs ? attrs() : undefined)
-      })
-    } else {
-      span = tracer.startSpan(spanName, attrs ? attrs() : undefined)
-    }
-  }
-
-  stack.push(span)
-  return span
-}
-
-export function stackPop(compiler: any, span: any, associatedName?: string) {
-  let stack = compilerStacks.get(compiler)
-  if (!stack) {
-    console.warn(
-      'Attempted to pop from non-existent stack. Compiler reference must be bad.'
-    )
-    return
-  }
-
-  let stoppedSpans: Set<Span> = compilerStoppedSpans.get(compiler)
-  if (!stoppedSpans) {
-    stoppedSpans = new Set()
-    compilerStoppedSpans.set(compiler, stoppedSpans)
-  }
-  if (stoppedSpans.has(span)) {
-    console.warn(
-      `Attempted to terminate tracing span that was already stopped for ${associatedName}`
-    )
-    return
-  }
-
-  while (true) {
-    let poppedSpan = stack.pop()
-
-    if (poppedSpan === span) {
-      stoppedSpans.add(poppedSpan)
-      span.end()
-      stoppedSpans.add(span)
-      break
-    } else if (poppedSpan === undefined || stack.indexOf(span) === -1) {
-      // We've either reached the top of the stack or the stack doesn't contain
-      // the span for another reason.
-      console.warn(`Tracing span was not found in stack for: ${associatedName}`)
-      stoppedSpans.add(span)
-      span.end()
-      break
-    } else if (stack.indexOf(span) !== -1) {
-      console.warn(
-        `Attempted to pop span that was not at top of stack for: ${associatedName}`
-      )
-      stoppedSpans.add(poppedSpan)
-      poppedSpan.end()
-    }
-  }
-}
-
-export function traceFn<T extends (...args: unknown[]) => ReturnType<T>>(
-  span: Span,
-  fn: T
-): ReturnType<T> {
-  return tracer.withSpan(span, () => {
-    try {
-      return fn()
-    } finally {
-      span.end()
-    }
-  })
-}
-
-export function traceAsyncFn<T extends (...args: unknown[]) => ReturnType<T>>(
-  span: Span,
-  fn: T
-): Promise<ReturnType<T>> {
-  return tracer.withSpan(span, async () => {
-    try {
-      return await fn()
-    } finally {
-      span.end()
-    }
-  })
-}
diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts
index 44b641e1f49c..1230f9f721b9 100644
--- a/packages/next/build/utils.ts
+++ b/packages/next/build/utils.ts
@@ -29,9 +29,8 @@ import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
 import { UnwrapPromise } from '../lib/coalesced-function'
 import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'
 import * as Log from './output/log'
-import opentelemetryApi from '@opentelemetry/api'
-import { tracer, traceAsyncFn } from './tracer'
 import { loadComponents } from '../next-server/server/load-components'
+import { trace } from '../telemetry/trace'
 
 const fileGzipStats: { [k: string]: Promise<number> } = {}
 const fsStatGzip = (file: string) => {
@@ -719,7 +718,7 @@ export async function isPageStatic(
   runtimeEnvConfig: any,
   locales?: string[],
   defaultLocale?: string,
-  spanContext?: any
+  parentId?: any
 ): Promise<{
   isStatic?: boolean
   isAmpOnly?: boolean
@@ -731,132 +730,115 @@ export async function isPageStatic(
   prerenderFallback?: boolean | 'blocking'
   isNextImageImported?: boolean
 }> {
-  return opentelemetryApi.context.with(
-    opentelemetryApi.propagation.extract(
-      opentelemetryApi.context.active(),
-      spanContext
-    ),
-    () => {
-      return traceAsyncFn(
-        tracer.startSpan('is-page-static-utils'),
-        async () => {
-          try {
-            require('../next-server/lib/runtime-config').setConfig(
-              runtimeEnvConfig
-            )
-            const components = await loadComponents(distDir, page, serverless)
-            const mod = components.ComponentMod
-            const Comp = mod.default || mod
-
-            if (
-              !Comp ||
-              !isValidElementType(Comp) ||
-              typeof Comp === 'string'
-            ) {
-              throw new Error('INVALID_DEFAULT_EXPORT')
-            }
-
-            const hasGetInitialProps = !!(Comp as any).getInitialProps
-            const hasStaticProps = !!(await mod.getStaticProps)
-            const hasStaticPaths = !!(await mod.getStaticPaths)
-            const hasServerProps = !!(await mod.getServerSideProps)
-            const hasLegacyServerProps = !!(await mod.unstable_getServerProps)
-            const hasLegacyStaticProps = !!(await mod.unstable_getStaticProps)
-            const hasLegacyStaticPaths = !!(await mod.unstable_getStaticPaths)
-            const hasLegacyStaticParams = !!(await mod.unstable_getStaticParams)
-
-            if (hasLegacyStaticParams) {
-              throw new Error(
-                `unstable_getStaticParams was replaced with getStaticPaths. Please update your code.`
-              )
-            }
-
-            if (hasLegacyStaticPaths) {
-              throw new Error(
-                `unstable_getStaticPaths was replaced with getStaticPaths. Please update your code.`
-              )
-            }
-
-            if (hasLegacyStaticProps) {
-              throw new Error(
-                `unstable_getStaticProps was replaced with getStaticProps. Please update your code.`
-              )
-            }
-
-            if (hasLegacyServerProps) {
-              throw new Error(
-                `unstable_getServerProps was replaced with getServerSideProps. Please update your code.`
-              )
-            }
-
-            // A page cannot be prerendered _and_ define a data requirement. That's
-            // contradictory!
-            if (hasGetInitialProps && hasStaticProps) {
-              throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT)
-            }
-
-            if (hasGetInitialProps && hasServerProps) {
-              throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT)
-            }
-
-            if (hasStaticProps && hasServerProps) {
-              throw new Error(SERVER_PROPS_SSG_CONFLICT)
-            }
-
-            const pageIsDynamic = isDynamicRoute(page)
-            // A page cannot have static parameters if it is not a dynamic page.
-            if (hasStaticProps && hasStaticPaths && !pageIsDynamic) {
-              throw new Error(
-                `getStaticPaths can only be used with dynamic pages, not '${page}'.` +
-                  `\nLearn more: https://nextjs.org/docs/routing/dynamic-routes`
-              )
-            }
-
-            if (hasStaticProps && pageIsDynamic && !hasStaticPaths) {
-              throw new Error(
-                `getStaticPaths is required for dynamic SSG pages and is missing for '${page}'.` +
-                  `\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value`
-              )
-            }
-
-            let prerenderRoutes: Array<string> | undefined
-            let encodedPrerenderRoutes: Array<string> | undefined
-            let prerenderFallback: boolean | 'blocking' | undefined
-            if (hasStaticProps && hasStaticPaths) {
-              ;({
-                paths: prerenderRoutes,
-                fallback: prerenderFallback,
-                encodedPaths: encodedPrerenderRoutes,
-              } = await buildStaticPaths(
-                page,
-                mod.getStaticPaths,
-                locales,
-                defaultLocale
-              ))
-            }
-
-            const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED
-            const config = mod.config || {}
-            return {
-              isStatic:
-                !hasStaticProps && !hasGetInitialProps && !hasServerProps,
-              isHybridAmp: config.amp === 'hybrid',
-              isAmpOnly: config.amp === true,
-              prerenderRoutes,
-              prerenderFallback,
-              encodedPrerenderRoutes,
-              hasStaticProps,
-              hasServerProps,
-              isNextImageImported,
-            }
-          } catch (err) {
-            if (err.code === 'MODULE_NOT_FOUND') return {}
-            throw err
-          }
-        }
-      )
+  const isPageStaticSpan = trace('is-page-static-utils', parentId)
+  return isPageStaticSpan.traceAsyncFn(async () => {
+    try {
+      require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
+      const components = await loadComponents(distDir, page, serverless)
+      const mod = components.ComponentMod
+      const Comp = mod.default || mod
+
+      if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') {
+        throw new Error('INVALID_DEFAULT_EXPORT')
+      }
+
+      const hasGetInitialProps = !!(Comp as any).getInitialProps
+      const hasStaticProps = !!(await mod.getStaticProps)
+      const hasStaticPaths = !!(await mod.getStaticPaths)
+      const hasServerProps = !!(await mod.getServerSideProps)
+      const hasLegacyServerProps = !!(await mod.unstable_getServerProps)
+      const hasLegacyStaticProps = !!(await mod.unstable_getStaticProps)
+      const hasLegacyStaticPaths = !!(await mod.unstable_getStaticPaths)
+      const hasLegacyStaticParams = !!(await mod.unstable_getStaticParams)
+
+      if (hasLegacyStaticParams) {
+        throw new Error(
+          `unstable_getStaticParams was replaced with getStaticPaths. Please update your code.`
+        )
+      }
+
+      if (hasLegacyStaticPaths) {
+        throw new Error(
+          `unstable_getStaticPaths was replaced with getStaticPaths. Please update your code.`
+        )
+      }
+
+      if (hasLegacyStaticProps) {
+        throw new Error(
+          `unstable_getStaticProps was replaced with getStaticProps. Please update your code.`
+        )
+      }
+
+      if (hasLegacyServerProps) {
+        throw new Error(
+          `unstable_getServerProps was replaced with getServerSideProps. Please update your code.`
+        )
+      }
+
+      // A page cannot be prerendered _and_ define a data requirement. That's
+      // contradictory!
+      if (hasGetInitialProps && hasStaticProps) {
+        throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT)
+      }
+
+      if (hasGetInitialProps && hasServerProps) {
+        throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT)
+      }
+
+      if (hasStaticProps && hasServerProps) {
+        throw new Error(SERVER_PROPS_SSG_CONFLICT)
+      }
+
+      const pageIsDynamic = isDynamicRoute(page)
+      // A page cannot have static parameters if it is not a dynamic page.
+      if (hasStaticProps && hasStaticPaths && !pageIsDynamic) {
+        throw new Error(
+          `getStaticPaths can only be used with dynamic pages, not '${page}'.` +
+            `\nLearn more: https://nextjs.org/docs/routing/dynamic-routes`
+        )
+      }
+
+      if (hasStaticProps && pageIsDynamic && !hasStaticPaths) {
+        throw new Error(
+          `getStaticPaths is required for dynamic SSG pages and is missing for '${page}'.` +
+            `\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value`
+        )
+      }
+
+      let prerenderRoutes: Array<string> | undefined
+      let encodedPrerenderRoutes: Array<string> | undefined
+      let prerenderFallback: boolean | 'blocking' | undefined
+      if (hasStaticProps && hasStaticPaths) {
+        ;({
+          paths: prerenderRoutes,
+          fallback: prerenderFallback,
+          encodedPaths: encodedPrerenderRoutes,
+        } = await buildStaticPaths(
+          page,
+          mod.getStaticPaths,
+          locales,
+          defaultLocale
+        ))
+      }
+
+      const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED
+      const config = mod.config || {}
+      return {
+        isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
+        isHybridAmp: config.amp === 'hybrid',
+        isAmpOnly: config.amp === true,
+        prerenderRoutes,
+        prerenderFallback,
+        encodedPrerenderRoutes,
+        hasStaticProps,
+        hasServerProps,
+        isNextImageImported,
+      }
+    } catch (err) {
+      if (err.code === 'MODULE_NOT_FOUND') return {}
+      throw err
     }
-  )
+  })
 }
 
 export async function hasCustomGetInitialProps(
diff --git a/packages/next/build/webpack/loaders/babel-loader/src/cache.js b/packages/next/build/webpack/loaders/babel-loader/src/cache.js
index 406b239a737e..a29a65a5a3f1 100644
--- a/packages/next/build/webpack/loaders/babel-loader/src/cache.js
+++ b/packages/next/build/webpack/loaders/babel-loader/src/cache.js
@@ -1,25 +1,23 @@
 import { createHash } from 'crypto'
-import { tracer, traceAsyncFn, traceFn } from '../../../../tracer'
+import { trace } from '../../../../../telemetry/trace'
 import transform from './transform'
 import cacache from 'next/dist/compiled/cacache'
 
 async function read(cacheDirectory, etag) {
-  const cachedResult = await traceAsyncFn(
-    tracer.startSpan('read-cache-file'),
-    async () => await cacache.get(cacheDirectory, etag)
+  const cachedResult = await trace('read-cache-file').traceAsyncFn(() =>
+    cacache.get(cacheDirectory, etag)
   )
-
   return JSON.parse(cachedResult.data)
 }
 
 function write(cacheDirectory, etag, data) {
-  return traceAsyncFn(tracer.startSpan('write-cache-file'), () =>
+  return trace('write-cache-file').traceAsyncFn(() =>
     cacache.put(cacheDirectory, etag, JSON.stringify(data))
   )
 }
 
 const etag = function (source, identifier, options) {
-  return traceFn(tracer.startSpan('etag'), () => {
+  return trace('etag').traceFn(() => {
     const hash = createHash('md4')
 
     const contents = JSON.stringify({ source, options, identifier })
@@ -31,8 +29,9 @@ const etag = function (source, identifier, options) {
 }
 
 export default async function handleCache(params) {
-  const span = tracer.startSpan('handle-cache')
-  return traceAsyncFn(span, async () => {
+  const span = trace('handle-cache')
+
+  return span.traceAsyncFn(async () => {
     const { source, options = {}, cacheIdentifier, cacheDirectory } = params
 
     const file = etag(source, cacheIdentifier)
@@ -47,12 +46,9 @@ export default async function handleCache(params) {
 
     // Otherwise just transform the file
     // return it to the user asap and write it in cache
-    const result = await traceAsyncFn(
-      tracer.startSpan('transform'),
-      async () => {
-        return transform(source, options)
-      }
-    )
+    const result = await trace('transform').traceAsyncFn(async () => {
+      return transform(source, options)
+    })
 
     await write(cacheDirectory, file, result)
 
diff --git a/packages/next/build/webpack/loaders/babel-loader/src/index.js b/packages/next/build/webpack/loaders/babel-loader/src/index.js
index 17234d6bad3a..9e87d5d58922 100644
--- a/packages/next/build/webpack/loaders/babel-loader/src/index.js
+++ b/packages/next/build/webpack/loaders/babel-loader/src/index.js
@@ -1,6 +1,6 @@
 // import babel from 'next/dist/compiled/babel/core'
 import loaderUtils from 'next/dist/compiled/loader-utils'
-import { tracer, traceAsyncFn, traceFn } from '../../../../tracer'
+import { trace } from '../../../../../telemetry/trace'
 import cache from './cache'
 import transform from './transform'
 
@@ -22,150 +22,142 @@ export default function makeLoader(callback) {
 }
 
 async function loader(source, inputSourceMap, overrides) {
-  // Provided by profiling-plugin.ts
-  return tracer.withSpan(this.currentTraceSpan, () => {
-    const span = tracer.startSpan('babel-loader')
-    return traceAsyncFn(span, async () => {
-      const filename = this.resourcePath
-      span.setAttribute('filename', filename)
-
-      let loaderOptions = loaderUtils.getOptions(this) || {}
-
-      let customOptions
-      if (overrides && overrides.customOptions) {
-        const result = await traceAsyncFn(
-          tracer.startSpan('loader-overrides-customoptions'),
+  // this.currentTraceSpan is provided by profiling-plugin.ts
+  const loaderSpan = trace('babel-loader', this.currentTraceSpan?.id)
+
+  return loaderSpan.traceAsyncFn(async () => {
+    const filename = this.resourcePath
+    loaderSpan.setAttribute('filename', filename)
+
+    let loaderOptions = loaderUtils.getOptions(this) || {}
+
+    let customOptions
+    if (overrides && overrides.customOptions) {
+      const customoptionsSpan = trace('loader-overrides-customoptions')
+      const result = await customoptionsSpan.traceAsyncFn(
+        async () =>
+          await overrides.customOptions.call(this, loaderOptions, {
+            source,
+            map: inputSourceMap,
+          })
+      )
+      customOptions = result.custom
+      loaderOptions = result.loader
+    }
+
+    // Standardize on 'sourceMaps' as the key passed through to Webpack, so that
+    // users may safely use either one alongside our default use of
+    // 'this.sourceMap' below without getting error about conflicting aliases.
+    if (
+      Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMap') &&
+      !Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMaps')
+    ) {
+      loaderOptions = Object.assign({}, loaderOptions, {
+        sourceMaps: loaderOptions.sourceMap,
+      })
+      delete loaderOptions.sourceMap
+    }
+
+    const programmaticOptions = Object.assign({}, loaderOptions, {
+      filename,
+      inputSourceMap: inputSourceMap || undefined,
+
+      // Set the default sourcemap behavior based on Webpack's mapping flag,
+      // but allow users to override if they want.
+      sourceMaps:
+        loaderOptions.sourceMaps === undefined
+          ? this.sourceMap
+          : loaderOptions.sourceMaps,
+
+      // Ensure that Webpack will get a full absolute path in the sourcemap
+      // so that it can properly map the module back to its internal cached
+      // modules.
+      sourceFileName: filename,
+      caller: {
+        name: 'babel-loader',
+
+        // Provide plugins with insight into webpack target.
+        // https://github.com/babel/babel-loader/issues/787
+        target: this.target,
+
+        // Webpack >= 2 supports ESM and dynamic import.
+        supportsStaticESM: true,
+        supportsDynamicImport: true,
+
+        // Webpack 5 supports TLA behind a flag. We enable it by default
+        // for Babel, and then webpack will throw an error if the experimental
+        // flag isn't enabled.
+        supportsTopLevelAwait: true,
+        ...loaderOptions.caller,
+      },
+    })
+    // Remove loader related options
+    delete programmaticOptions.cacheDirectory
+    delete programmaticOptions.cacheIdentifier
+
+    const partialConfigSpan = trace('babel-load-partial-config-async')
+    const config = partialConfigSpan.traceFn(() => {
+      return babel.loadPartialConfig(programmaticOptions)
+    })
+
+    if (config) {
+      let options = config.options
+      if (overrides && overrides.config) {
+        const overridesSpan = trace('loader-overrides-config')
+        options = await overridesSpan.traceAsyncFn(
           async () =>
-            await overrides.customOptions.call(this, loaderOptions, {
+            await overrides.config.call(this, config, {
               source,
               map: inputSourceMap,
+              customOptions,
             })
         )
-        customOptions = result.custom
-        loaderOptions = result.loader
       }
 
-      // Standardize on 'sourceMaps' as the key passed through to Webpack, so that
-      // users may safely use either one alongside our default use of
-      // 'this.sourceMap' below without getting error about conflicting aliases.
-      if (
-        Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMap') &&
-        !Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMaps')
-      ) {
-        loaderOptions = Object.assign({}, loaderOptions, {
-          sourceMaps: loaderOptions.sourceMap,
+      if (options.sourceMaps === 'inline') {
+        // Babel has this weird behavior where if you set "inline", we
+        // inline the sourcemap, and set 'result.map = null'. This results
+        // in bad behavior from Babel since the maps get put into the code,
+        // which Webpack does not expect, and because the map we return to
+        // Webpack is null, which is also bad. To avoid that, we override the
+        // behavior here so "inline" just behaves like 'true'.
+        options.sourceMaps = true
+      }
+
+      const { cacheDirectory, cacheIdentifier } = loaderOptions
+
+      let result
+      if (cacheDirectory) {
+        result = await cache({
+          source,
+          options,
+          cacheDirectory,
+          cacheIdentifier,
+          cacheCompression: false,
+        })
+      } else {
+        const transformSpan = trace('transform')
+        transformSpan.setAttribute('filename', filename)
+        transformSpan.setAttribute('cache', 'DISABLED')
+        result = await transformSpan.traceAsyncFn(async () => {
+          return transform(source, options)
         })
-        delete loaderOptions.sourceMap
       }
 
-      const programmaticOptions = Object.assign({}, loaderOptions, {
-        filename,
-        inputSourceMap: inputSourceMap || undefined,
-
-        // Set the default sourcemap behavior based on Webpack's mapping flag,
-        // but allow users to override if they want.
-        sourceMaps:
-          loaderOptions.sourceMaps === undefined
-            ? this.sourceMap
-            : loaderOptions.sourceMaps,
-
-        // Ensure that Webpack will get a full absolute path in the sourcemap
-        // so that it can properly map the module back to its internal cached
-        // modules.
-        sourceFileName: filename,
-        caller: {
-          name: 'babel-loader',
-
-          // Provide plugins with insight into webpack target.
-          // https://github.com/babel/babel-loader/issues/787
-          target: this.target,
-
-          // Webpack >= 2 supports ESM and dynamic import.
-          supportsStaticESM: true,
-          supportsDynamicImport: true,
-
-          // Webpack 5 supports TLA behind a flag. We enable it by default
-          // for Babel, and then webpack will throw an error if the experimental
-          // flag isn't enabled.
-          supportsTopLevelAwait: true,
-          ...loaderOptions.caller,
-        },
-      })
-      // Remove loader related options
-      delete programmaticOptions.cacheDirectory
-      delete programmaticOptions.cacheIdentifier
-
-      const config = traceFn(
-        tracer.startSpan('babel-load-partial-config-async'),
-        () => {
-          return babel.loadPartialConfig(programmaticOptions)
-        }
-      )
+      // TODO: Babel should really provide the full list of config files that
+      // were used so that this can also handle files loaded with 'extends'.
+      if (typeof config.babelrc === 'string') {
+        this.addDependency(config.babelrc)
+      }
 
-      if (config) {
-        let options = config.options
-        if (overrides && overrides.config) {
-          options = await traceAsyncFn(
-            tracer.startSpan('loader-overrides-config'),
-            async () =>
-              await overrides.config.call(this, config, {
-                source,
-                map: inputSourceMap,
-                customOptions,
-              })
-          )
-        }
-
-        if (options.sourceMaps === 'inline') {
-          // Babel has this weird behavior where if you set "inline", we
-          // inline the sourcemap, and set 'result.map = null'. This results
-          // in bad behavior from Babel since the maps get put into the code,
-          // which Webpack does not expect, and because the map we return to
-          // Webpack is null, which is also bad. To avoid that, we override the
-          // behavior here so "inline" just behaves like 'true'.
-          options.sourceMaps = true
-        }
-
-        const { cacheDirectory, cacheIdentifier } = loaderOptions
-
-        let result
-        if (cacheDirectory) {
-          result = await cache({
-            source,
-            options,
-            cacheDirectory,
-            cacheIdentifier,
-            cacheCompression: false,
-          })
-        } else {
-          result = await traceAsyncFn(
-            tracer.startSpan('transform', {
-              attributes: {
-                filename,
-                cache: 'DISABLED',
-              },
-            }),
-            async () => {
-              return transform(source, options)
-            }
-          )
-        }
-
-        // TODO: Babel should really provide the full list of config files that
-        // were used so that this can also handle files loaded with 'extends'.
-        if (typeof config.babelrc === 'string') {
-          this.addDependency(config.babelrc)
-        }
-
-        if (result) {
-          const { code, map } = result
-
-          return [code, map]
-        }
+      if (result) {
+        const { code, map } = result
+
+        return [code, map]
       }
+    }
 
-      // If the file was ignored, pass through the original content.
-      return [source, inputSourceMap]
-    })
+    // If the file was ignored, pass through the original content.
+    return [source, inputSourceMap]
   })
 }
diff --git a/packages/next/build/webpack/loaders/next-client-pages-loader.ts b/packages/next/build/webpack/loaders/next-client-pages-loader.ts
index cb7725fb03cc..b1cf19d7cf97 100644
--- a/packages/next/build/webpack/loaders/next-client-pages-loader.ts
+++ b/packages/next/build/webpack/loaders/next-client-pages-loader.ts
@@ -1,5 +1,4 @@
 import loaderUtils from 'next/dist/compiled/loader-utils'
-import { tracer, traceFn } from '../../tracer'
 
 export type ClientPagesLoaderOptions = {
   absolutePagePath: string
@@ -8,27 +7,28 @@ export type ClientPagesLoaderOptions = {
 
 // this parameter: https://www.typescriptlang.org/docs/handbook/functions.html#this-parameters
 function nextClientPagesLoader(this: any) {
-  return tracer.withSpan(this.currentTraceSpan, () => {
-    const span = tracer.startSpan('next-client-pages-loader')
-    return traceFn(span, () => {
-      const { absolutePagePath, page } = loaderUtils.getOptions(
-        this
-      ) as ClientPagesLoaderOptions
+  const pagesLoaderSpan = this.currentTraceSpan.traceChild(
+    'next-client-pages-loader'
+  )
 
-      span.setAttribute('absolutePagePath', absolutePagePath)
+  return pagesLoaderSpan.traceFn(() => {
+    const { absolutePagePath, page } = loaderUtils.getOptions(
+      this
+    ) as ClientPagesLoaderOptions
 
-      const stringifiedAbsolutePagePath = JSON.stringify(absolutePagePath)
-      const stringifiedPage = JSON.stringify(page)
+    pagesLoaderSpan.setAttribute('absolutePagePath', absolutePagePath)
 
-      return `
-      (window.__NEXT_P = window.__NEXT_P || []).push([
-        ${stringifiedPage},
-        function () {
-          return require(${stringifiedAbsolutePagePath});
-        }
-      ]);
-    `
-    })
+    const stringifiedAbsolutePagePath = JSON.stringify(absolutePagePath)
+    const stringifiedPage = JSON.stringify(page)
+
+    return `
+    (window.__NEXT_P = window.__NEXT_P || []).push([
+      ${stringifiedPage},
+      function () {
+        return require(${stringifiedAbsolutePagePath});
+      }
+    ]);
+  `
   })
 }
 
diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/index.ts b/packages/next/build/webpack/loaders/next-serverless-loader/index.ts
index 2dd6f884153a..4db9d06268a8 100644
--- a/packages/next/build/webpack/loaders/next-serverless-loader/index.ts
+++ b/packages/next/build/webpack/loaders/next-serverless-loader/index.ts
@@ -11,7 +11,7 @@ import {
   ROUTES_MANIFEST,
   REACT_LOADABLE_MANIFEST,
 } from '../../../../next-server/lib/constants'
-import { tracer, traceFn } from '../../../tracer'
+import { trace } from '../../../../telemetry/trace'
 
 export type ServerlessLoaderQuery = {
   page: string
@@ -34,8 +34,8 @@ export type ServerlessLoaderQuery = {
 }
 
 const nextServerlessLoader: webpack.loader.Loader = function () {
-  const span = tracer.startSpan('next-serverless-loader')
-  return traceFn(span, () => {
+  const loaderSpan = trace('next-serverless-loader')
+  return loaderSpan.traceFn(() => {
     const {
       distDir,
       absolutePagePath,
diff --git a/packages/next/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/build/webpack/plugins/build-manifest-plugin.ts
index 5cfe31ba85d6..4309753153cc 100644
--- a/packages/next/build/webpack/plugins/build-manifest-plugin.ts
+++ b/packages/next/build/webpack/plugins/build-manifest-plugin.ts
@@ -17,7 +17,6 @@ import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-e
 import { ampFirstEntryNamesMap } from './next-drop-client-page-plugin'
 import { Rewrite } from '../../../lib/load-custom-routes'
 import { getSortedRoutes } from '../../../next-server/lib/router/utils'
-import { tracer, traceFn } from '../../tracer'
 import { spans } from './profiling-plugin'
 
 type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> }
@@ -31,37 +30,39 @@ function generateClientManifest(
   assetMap: BuildManifest,
   rewrites: Rewrite[]
 ): string {
-  return tracer.withSpan(spans.get(compiler), () => {
-    const span = tracer.startSpan('NextJsBuildManifest-generateClientManifest')
-    return traceFn(span, () => {
-      const clientManifest: ClientBuildManifest = {
-        // TODO: update manifest type to include rewrites
-        __rewrites: rewrites as any,
+  const compilerSpan = spans.get(compiler)
+  const genClientManifestSpan = compilerSpan?.traceChild(
+    'NextJsBuildManifest-generateClientManifest'
+  )
+
+  return genClientManifestSpan?.traceFn(() => {
+    const clientManifest: ClientBuildManifest = {
+      // TODO: update manifest type to include rewrites
+      __rewrites: rewrites as any,
+    }
+    const appDependencies = new Set(assetMap.pages['/_app'])
+    const sortedPageKeys = getSortedRoutes(Object.keys(assetMap.pages))
+
+    sortedPageKeys.forEach((page) => {
+      const dependencies = assetMap.pages[page]
+
+      if (page === '/_app') return
+      // Filter out dependencies in the _app entry, because those will have already
+      // been loaded by the client prior to a navigation event
+      const filteredDeps = dependencies.filter(
+        (dep) => !appDependencies.has(dep)
+      )
+
+      // The manifest can omit the page if it has no requirements
+      if (filteredDeps.length) {
+        clientManifest[page] = filteredDeps
       }
-      const appDependencies = new Set(assetMap.pages['/_app'])
-      const sortedPageKeys = getSortedRoutes(Object.keys(assetMap.pages))
-
-      sortedPageKeys.forEach((page) => {
-        const dependencies = assetMap.pages[page]
-
-        if (page === '/_app') return
-        // Filter out dependencies in the _app entry, because those will have already
-        // been loaded by the client prior to a navigation event
-        const filteredDeps = dependencies.filter(
-          (dep) => !appDependencies.has(dep)
-        )
-
-        // The manifest can omit the page if it has no requirements
-        if (filteredDeps.length) {
-          clientManifest[page] = filteredDeps
-        }
-      })
-      // provide the sorted pages as an array so we don't rely on the object's keys
-      // being in order and we don't slow down look-up time for page assets
-      clientManifest.sortedPages = sortedPageKeys
-
-      return devalue(clientManifest)
     })
+    // provide the sorted pages as an array so we don't rely on the object's keys
+    // being in order and we don't slow down look-up time for page assets
+    clientManifest.sortedPages = sortedPageKeys
+
+    return devalue(clientManifest)
   })
 }
 
@@ -103,125 +104,125 @@ export default class BuildManifestPlugin {
   }
 
   createAssets(compiler: any, compilation: any, assets: any) {
-    return tracer.withSpan(spans.get(compiler), () => {
-      const span = tracer.startSpan('NextJsBuildManifest-createassets')
-      return traceFn(span, () => {
-        const namedChunks: Map<string, webpack.compilation.Chunk> =
-          compilation.namedChunks
-        const assetMap: DeepMutable<BuildManifest> = {
-          polyfillFiles: [],
-          devFiles: [],
-          ampDevFiles: [],
-          lowPriorityFiles: [],
-          pages: { '/_app': [] },
-          ampFirstPages: [],
-        }
-
-        const ampFirstEntryNames = ampFirstEntryNamesMap.get(compilation)
-        if (ampFirstEntryNames) {
-          for (const entryName of ampFirstEntryNames) {
-            const pagePath = getRouteFromEntrypoint(entryName)
-            if (!pagePath) {
-              continue
-            }
+    const compilerSpan = spans.get(compiler)
+    const createAssetsSpan = compilerSpan?.traceChild(
+      'NextJsBuildManifest-createassets'
+    )
+    return createAssetsSpan?.traceFn(() => {
+      const namedChunks: Map<string, webpack.compilation.Chunk> =
+        compilation.namedChunks
+      const assetMap: DeepMutable<BuildManifest> = {
+        polyfillFiles: [],
+        devFiles: [],
+        ampDevFiles: [],
+        lowPriorityFiles: [],
+        pages: { '/_app': [] },
+        ampFirstPages: [],
+      }
 
-            assetMap.ampFirstPages.push(pagePath)
+      const ampFirstEntryNames = ampFirstEntryNamesMap.get(compilation)
+      if (ampFirstEntryNames) {
+        for (const entryName of ampFirstEntryNames) {
+          const pagePath = getRouteFromEntrypoint(entryName)
+          if (!pagePath) {
+            continue
           }
-        }
-
-        const mainJsChunk = namedChunks.get(CLIENT_STATIC_FILES_RUNTIME_MAIN)
 
-        const mainJsFiles: string[] = getFilesArray(mainJsChunk?.files).filter(
-          isJsFile
-        )
-
-        const polyfillChunk = namedChunks.get(
-          CLIENT_STATIC_FILES_RUNTIME_POLYFILLS
-        )
-
-        // Create a separate entry  for polyfills
-        assetMap.polyfillFiles = getFilesArray(polyfillChunk?.files).filter(
-          isJsFile
-        )
+          assetMap.ampFirstPages.push(pagePath)
+        }
+      }
 
-        const reactRefreshChunk = namedChunks.get(
-          CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH
-        )
-        assetMap.devFiles = getFilesArray(reactRefreshChunk?.files).filter(
-          isJsFile
-        )
+      const mainJsChunk = namedChunks.get(CLIENT_STATIC_FILES_RUNTIME_MAIN)
 
-        for (const entrypoint of compilation.entrypoints.values()) {
-          const isAmpRuntime =
-            entrypoint.name === CLIENT_STATIC_FILES_RUNTIME_AMP
+      const mainJsFiles: string[] = getFilesArray(mainJsChunk?.files).filter(
+        isJsFile
+      )
 
-          if (isAmpRuntime) {
-            for (const file of entrypoint.getFiles()) {
-              if (!(isJsFile(file) || file.endsWith('.css'))) {
-                continue
-              }
+      const polyfillChunk = namedChunks.get(
+        CLIENT_STATIC_FILES_RUNTIME_POLYFILLS
+      )
 
-              assetMap.ampDevFiles.push(file.replace(/\\/g, '/'))
-            }
-            continue
-          }
-          const pagePath = getRouteFromEntrypoint(entrypoint.name)
+      // Create a separate entry  for polyfills
+      assetMap.polyfillFiles = getFilesArray(polyfillChunk?.files).filter(
+        isJsFile
+      )
 
-          if (!pagePath) {
-            continue
-          }
+      const reactRefreshChunk = namedChunks.get(
+        CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH
+      )
+      assetMap.devFiles = getFilesArray(reactRefreshChunk?.files).filter(
+        isJsFile
+      )
 
-          const filesForEntry: string[] = []
+      for (const entrypoint of compilation.entrypoints.values()) {
+        const isAmpRuntime = entrypoint.name === CLIENT_STATIC_FILES_RUNTIME_AMP
 
-          // getFiles() - helper function to read the files for an entrypoint from stats object
+        if (isAmpRuntime) {
           for (const file of entrypoint.getFiles()) {
             if (!(isJsFile(file) || file.endsWith('.css'))) {
               continue
             }
 
-            filesForEntry.push(file.replace(/\\/g, '/'))
+            assetMap.ampDevFiles.push(file.replace(/\\/g, '/'))
           }
-
-          assetMap.pages[pagePath] = [...mainJsFiles, ...filesForEntry]
+          continue
         }
+        const pagePath = getRouteFromEntrypoint(entrypoint.name)
 
-        // Add the runtime build manifest file (generated later in this file)
-        // as a dependency for the app. If the flag is false, the file won't be
-        // downloaded by the client.
-        assetMap.lowPriorityFiles.push(
-          `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
-        )
-
-        // Add the runtime ssg manifest file as a lazy-loaded file dependency.
-        // We also stub this file out for development mode (when it is not
-        // generated).
-        const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`
-
-        const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js`
-        assetMap.lowPriorityFiles.push(ssgManifestPath)
-        assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest)
+        if (!pagePath) {
+          continue
+        }
 
-        assetMap.pages = Object.keys(assetMap.pages)
-          .sort()
-          // eslint-disable-next-line
-          .reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any)
+        const filesForEntry: string[] = []
 
-        assets[BUILD_MANIFEST] = new sources.RawSource(
-          JSON.stringify(assetMap, null, 2)
-        )
+        // getFiles() - helper function to read the files for an entrypoint from stats object
+        for (const file of entrypoint.getFiles()) {
+          if (!(isJsFile(file) || file.endsWith('.css'))) {
+            continue
+          }
 
-        const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
+          filesForEntry.push(file.replace(/\\/g, '/'))
+        }
 
-        assets[clientManifestPath] = new sources.RawSource(
-          `self.__BUILD_MANIFEST = ${generateClientManifest(
-            compiler,
-            assetMap,
-            this.rewrites
-          )};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
-        )
+        assetMap.pages[pagePath] = [...mainJsFiles, ...filesForEntry]
+      }
 
-        return assets
-      })
+      // Add the runtime build manifest file (generated later in this file)
+      // as a dependency for the app. If the flag is false, the file won't be
+      // downloaded by the client.
+      assetMap.lowPriorityFiles.push(
+        `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
+      )
+
+      // Add the runtime ssg manifest file as a lazy-loaded file dependency.
+      // We also stub this file out for development mode (when it is not
+      // generated).
+      const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`
+
+      const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js`
+      assetMap.lowPriorityFiles.push(ssgManifestPath)
+      assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest)
+
+      assetMap.pages = Object.keys(assetMap.pages)
+        .sort()
+        // eslint-disable-next-line
+        .reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any)
+
+      assets[BUILD_MANIFEST] = new sources.RawSource(
+        JSON.stringify(assetMap, null, 2)
+      )
+
+      const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
+
+      assets[clientManifestPath] = new sources.RawSource(
+        `self.__BUILD_MANIFEST = ${generateClientManifest(
+          compiler,
+          assetMap,
+          this.rewrites
+        )};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
+      )
+
+      return assets
     })
   }
 
diff --git a/packages/next/build/webpack/plugins/build-stats-plugin.ts b/packages/next/build/webpack/plugins/build-stats-plugin.ts
index bc8a6daf4c91..e41b0c67b466 100644
--- a/packages/next/build/webpack/plugins/build-stats-plugin.ts
+++ b/packages/next/build/webpack/plugins/build-stats-plugin.ts
@@ -4,7 +4,7 @@ import path from 'path'
 import bfj from 'next/dist/compiled/bfj'
 import { spans } from './profiling-plugin'
 import { webpack } from 'next/dist/compiled/webpack/webpack'
-import { tracer, traceAsyncFn } from '../../tracer'
+import { trace } from '../../../telemetry/trace'
 
 const STATS_VERSION = 0
 
@@ -75,46 +75,45 @@ export default class BuildStatsPlugin {
     compiler.hooks.done.tapAsync(
       'NextJsBuildStats',
       async (stats, callback) => {
-        tracer.withSpan(spans.get(compiler), async () => {
-          try {
-            const writeStatsSpan = tracer.startSpan('NextJsBuildStats')
-            await traceAsyncFn(writeStatsSpan, () => {
-              return new Promise((resolve, reject) => {
-                const statsJson = reduceSize(
-                  stats.toJson({
-                    all: false,
-                    cached: true,
-                    reasons: true,
-                    entrypoints: true,
-                    chunks: true,
-                    errors: false,
-                    warnings: false,
-                    maxModules: Infinity,
-                    chunkModules: true,
-                    modules: true,
-                    // @ts-ignore this option exists
-                    ids: true,
-                  })
-                )
-                const fileStream = fs.createWriteStream(
-                  path.join(this.distDir, 'next-stats.json')
-                )
-                const jsonStream = bfj.streamify({
-                  version: STATS_VERSION,
-                  stats: statsJson,
+        const compilerSpan = spans.get(compiler)
+        try {
+          const writeStatsSpan = trace('NextJsBuildStats', compilerSpan?.id)
+          await writeStatsSpan.traceAsyncFn(() => {
+            return new Promise((resolve, reject) => {
+              const statsJson = reduceSize(
+                stats.toJson({
+                  all: false,
+                  cached: true,
+                  reasons: true,
+                  entrypoints: true,
+                  chunks: true,
+                  errors: false,
+                  warnings: false,
+                  maxModules: Infinity,
+                  chunkModules: true,
+                  modules: true,
+                  // @ts-ignore this option exists
+                  ids: true,
                 })
-                jsonStream.pipe(fileStream)
-                jsonStream.on('error', reject)
-                fileStream.on('error', reject)
-                jsonStream.on('dataError', reject)
-                fileStream.on('close', resolve)
+              )
+              const fileStream = fs.createWriteStream(
+                path.join(this.distDir, 'next-stats.json')
+              )
+              const jsonStream = bfj.streamify({
+                version: STATS_VERSION,
+                stats: statsJson,
               })
+              jsonStream.pipe(fileStream)
+              jsonStream.on('error', reject)
+              fileStream.on('error', reject)
+              jsonStream.on('dataError', reject)
+              fileStream.on('close', resolve)
             })
-            callback()
-          } catch (err) {
-            callback(err)
-          }
-        })
+          })
+          callback()
+        } catch (err) {
+          callback(err)
+        }
       }
     )
   }
diff --git a/packages/next/build/webpack/plugins/css-minimizer-plugin.ts b/packages/next/build/webpack/plugins/css-minimizer-plugin.ts
index b417fdbb3c88..2e2b7a69ebba 100644
--- a/packages/next/build/webpack/plugins/css-minimizer-plugin.ts
+++ b/packages/next/build/webpack/plugins/css-minimizer-plugin.ts
@@ -6,7 +6,7 @@ import {
   isWebpack5,
   sources,
 } from 'next/dist/compiled/webpack/webpack'
-import { tracer, traceAsyncFn } from '../../tracer'
+import { trace } from '../../../telemetry/trace'
 import { spans } from './profiling-plugin'
 
 // https://github.com/NMFR/optimize-css-assets-webpack-plugin/blob/0a410a9bf28c7b0e81a3470a13748e68ca2f50aa/src/index.js#L20
@@ -70,47 +70,44 @@ export class CssMinimizerPlugin {
             stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
           },
           async (assets: any) => {
-            return tracer.withSpan(spans.get(compiler), () => {
-              const span = tracer.startSpan('css-minimizer-plugin', {
-                attributes: {
-                  webpackVersion: 5,
-                },
-              })
-
-              return traceAsyncFn(span, async () => {
-                const files = Object.keys(assets)
-                await Promise.all(
-                  files
-                    .filter((file) => CSS_REGEX.test(file))
-                    .map(async (file) => {
-                      const assetSpan = tracer.startSpan('minify-css', {
-                        attributes: {
-                          file,
-                        },
-                      })
-                      return traceAsyncFn(span, async () => {
-                        const asset = assets[file]
-
-                        const etag = cache.getLazyHashedEtag(asset)
-
-                        const cachedResult = await cache.getPromise(file, etag)
-
-                        assetSpan.setAttribute(
-                          'cache',
-                          cachedResult ? 'HIT' : 'MISS'
-                        )
-                        if (cachedResult) {
-                          assets[file] = cachedResult
-                          return
-                        }
-
-                        const result = await this.optimizeAsset(file, asset)
-                        await cache.storePromise(file, etag, result)
-                        assets[file] = result
-                      })
+            const compilerSpan = spans.get(compiler)
+            const cssMinimizerSpan = trace(
+              'css-minimizer-plugin',
+              compilerSpan?.id
+            )
+            cssMinimizerSpan.setAttribute('webpackVersion', 5)
+
+            return cssMinimizerSpan.traceAsyncFn(async () => {
+              const files = Object.keys(assets)
+              await Promise.all(
+                files
+                  .filter((file) => CSS_REGEX.test(file))
+                  .map(async (file) => {
+                    const assetSpan = trace('minify-css', cssMinimizerSpan.id)
+                    assetSpan.setAttribute('file', file)
+
+                    return assetSpan.traceAsyncFn(async () => {
+                      const asset = assets[file]
+
+                      const etag = cache.getLazyHashedEtag(asset)
+
+                      const cachedResult = await cache.getPromise(file, etag)
+
+                      assetSpan.setAttribute(
+                        'cache',
+                        cachedResult ? 'HIT' : 'MISS'
+                      )
+                      if (cachedResult) {
+                        assets[file] = cachedResult
+                        return
+                      }
+
+                      const result = await this.optimizeAsset(file, asset)
+                      await cache.storePromise(file, etag, result)
+                      assets[file] = result
                     })
-                )
-              })
+                  })
+              )
             })
           }
         )
@@ -119,42 +116,38 @@ export class CssMinimizerPlugin {
       compilation.hooks.optimizeChunkAssets.tapPromise(
         'CssMinimizerPlugin',
         (chunks: webpack.compilation.Chunk[]) => {
-          return tracer.withSpan(spans.get(compiler), () => {
-            const span = tracer.startSpan('css-minimizer-plugin', {
-              attributes: {
-                webpackVersion: 4,
-                compilationName: compilation.name,
-              },
-            })
-
-            return traceAsyncFn(span, async () => {
-              const res = await Promise.all(
-                chunks
-                  .reduce(
-                    (acc, chunk) => acc.concat(chunk.files || []),
-                    [] as string[]
-                  )
-                  .filter((entry) => CSS_REGEX.test(entry))
-                  .map(async (file) => {
-                    const assetSpan = tracer.startSpan('minify-css', {
-                      attributes: {
-                        file,
-                      },
-                    })
-                    return traceAsyncFn(assetSpan, async () => {
-                      const asset = compilation.assets[file]
-                      // Makes trace attributes the same as webpack 5
-                      // When using webpack 4 the result is not cached
-                      assetSpan.setAttribute('cache', 'MISS')
-                      compilation.assets[file] = await this.optimizeAsset(
-                        file,
-                        asset
-                      )
-                    })
+          const compilerSpan = spans.get(compiler)
+          const cssMinimizerSpan = trace(
+            'css-minimizer-plugin',
+            compilerSpan?.id
+          )
+          cssMinimizerSpan.setAttribute('webpackVersion', 4)
+          cssMinimizerSpan.setAttribute('compilationName', compilation.name)
+
+          return cssMinimizerSpan.traceAsyncFn(async () => {
+            return await Promise.all(
+              chunks
+                .reduce(
+                  (acc, chunk) => acc.concat(chunk.files || []),
+                  [] as string[]
+                )
+                .filter((entry) => CSS_REGEX.test(entry))
+                .map(async (file) => {
+                  const assetSpan = trace('minify-css', cssMinimizerSpan.id)
+                  assetSpan.setAttribute('file', file)
+
+                  return assetSpan.traceAsyncFn(async () => {
+                    const asset = compilation.assets[file]
+                    // Makes trace attributes the same as webpack 5
+                    // When using webpack 4 the result is not cached
+                    assetSpan.setAttribute('cache', 'MISS')
+                    compilation.assets[file] = await this.optimizeAsset(
+                      file,
+                      asset
+                    )
                   })
-              )
-              return res
-            })
+                })
+            )
           })
         }
       )
diff --git a/packages/next/build/webpack/plugins/profiling-plugin.ts b/packages/next/build/webpack/plugins/profiling-plugin.ts
index 146ca81d09be..8bab027fcec6 100644
--- a/packages/next/build/webpack/plugins/profiling-plugin.ts
+++ b/packages/next/build/webpack/plugins/profiling-plugin.ts
@@ -1,14 +1,8 @@
-import { tracer, stackPush, stackPop } from '../../tracer'
 import { webpack, isWebpack5 } from 'next/dist/compiled/webpack/webpack'
-import {
-  Span,
-  trace,
-  ProxyTracerProvider,
-  NoopTracerProvider,
-} from '@opentelemetry/api'
+import { trace, stackPush, stackPop, Span } from '../../../telemetry/trace'
 
 const pluginName = 'ProfilingPlugin'
-export const spans = new WeakMap()
+export const spans = new WeakMap<any, Span>()
 
 function getNormalModuleLoaderHook(compilation: any) {
   if (isWebpack5) {
@@ -19,23 +13,10 @@ function getNormalModuleLoaderHook(compilation: any) {
   return compilation.hooks.normalModuleLoader
 }
 
-function tracingIsEnabled() {
-  const tracerProvider: any = trace.getTracerProvider()
-  if (tracerProvider instanceof ProxyTracerProvider) {
-    const proxyDelegate: any = tracerProvider.getDelegate()
-    return !(proxyDelegate instanceof NoopTracerProvider)
-  }
-  return false
-}
-
 export class ProfilingPlugin {
   compiler: any
 
   apply(compiler: any) {
-    // Only enable plugin when instrumentation is loaded
-    if (!tracingIsEnabled()) {
-      return
-    }
     this.traceTopLevelHooks(compiler)
     this.traceCompilationHooks(compiler)
     this.compiler = compiler
@@ -46,7 +27,7 @@ export class ProfilingPlugin {
     startHook: any,
     stopHook: any,
     attrs?: any,
-    onSetSpan?: (span: Span | undefined) => void
+    onSetSpan?: (span: Span) => void
   ) {
     let span: Span | undefined
     startHook.tap(pluginName, () => {
@@ -60,7 +41,7 @@ export class ProfilingPlugin {
       if (!span) {
         return
       }
-      stackPop(this.compiler, span, spanName)
+      stackPop(this.compiler, span)
     })
   }
 
@@ -72,7 +53,7 @@ export class ProfilingPlugin {
       }
     })
     stopHook.tap(pluginName, () => {
-      stackPop(this.compiler, span, spanName)
+      stackPop(this.compiler, span)
     })
   }
 
@@ -82,7 +63,7 @@ export class ProfilingPlugin {
       compiler.hooks.compile,
       compiler.hooks.done,
       () => {
-        return { attributes: { name: compiler.name } }
+        return { name: compiler.name }
       },
       (span) => spans.set(compiler, span)
     )
@@ -115,11 +96,10 @@ export class ProfilingPlugin {
         if (!compilerSpan) {
           return
         }
-        tracer.withSpan(compilerSpan, () => {
-          const span = tracer.startSpan('build-module')
-          span.setAttribute('name', module.userRequest)
-          spans.set(module, span)
-        })
+
+        const span = trace('build-module', compilerSpan.id)
+        span.setAttribute('name', module.userRequest)
+        spans.set(module, span)
       })
 
       getNormalModuleLoaderHook(compilation).tap(
@@ -131,7 +111,7 @@ export class ProfilingPlugin {
       )
 
       compilation.hooks.succeedModule.tap(pluginName, (module: any) => {
-        spans.get(module).end()
+        spans.get(module)?.stop()
       })
 
       this.traceHookPair(
diff --git a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js
index ca72c47b5351..720d068d11ab 100644
--- a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js
+++ b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js
@@ -10,7 +10,6 @@ import pLimit from 'p-limit'
 import jestWorker from 'jest-worker'
 import crypto from 'crypto'
 import cacache from 'next/dist/compiled/cacache'
-import { tracer, traceAsyncFn } from '../../../../tracer'
 import { spans } from '../../profiling-plugin'
 
 function getEcmaVersion(environment) {
@@ -123,193 +122,187 @@ class TerserPlugin {
     { SourceMapSource, RawSource }
   ) {
     const compilerSpan = spans.get(compiler)
+    const terserSpan = compilerSpan.traceChild('terser-webpack-plugin-optimize')
+    terserSpan.setAttribute('webpackVersion', isWebpack5 ? 5 : 4)
+    terserSpan.setAttribute('compilationName', compilation.name)
+
+    return terserSpan.traceAsyncFn(async () => {
+      let numberOfAssetsForMinify = 0
+      const assetsList = isWebpack5
+        ? Object.keys(assets)
+        : [
+            ...Array.from(compilation.additionalChunkAssets || []),
+            ...Array.from(assets).reduce((acc, chunk) => {
+              return acc.concat(Array.from(chunk.files || []))
+            }, []),
+          ]
+
+      const assetsForMinify = await Promise.all(
+        assetsList
+          .filter((name) => {
+            if (
+              !ModuleFilenameHelpers.matchObject.bind(
+                // eslint-disable-next-line no-undefined
+                undefined,
+                { test: /\.[cm]?js(\?.*)?$/i }
+              )(name)
+            ) {
+              return false
+            }
 
-    return tracer.withSpan(compilerSpan, () => {
-      const span = tracer.startSpan('terser-webpack-plugin-optimize', {
-        attributes: {
-          webpackVersion: isWebpack5 ? 5 : 4,
-          compilationName: compilation.name,
-        },
-      })
-
-      return traceAsyncFn(span, async () => {
-        let numberOfAssetsForMinify = 0
-        const assetsList = isWebpack5
-          ? Object.keys(assets)
-          : [
-              ...Array.from(compilation.additionalChunkAssets || []),
-              ...Array.from(assets).reduce((acc, chunk) => {
-                return acc.concat(Array.from(chunk.files || []))
-              }, []),
-            ]
-
-        const assetsForMinify = await Promise.all(
-          assetsList
-            .filter((name) => {
-              if (
-                !ModuleFilenameHelpers.matchObject.bind(
-                  // eslint-disable-next-line no-undefined
-                  undefined,
-                  { test: /\.[cm]?js(\?.*)?$/i }
-                )(name)
-              ) {
-                return false
-              }
+            const res = compilation.getAsset(name)
+            if (!res) {
+              console.log(name)
+              return false
+            }
 
-              const res = compilation.getAsset(name)
-              if (!res) {
-                console.log(name)
-                return false
-              }
+            const { info } = res
 
-              const { info } = res
+            // Skip double minimize assets from child compilation
+            if (info.minimized) {
+              return false
+            }
 
-              // Skip double minimize assets from child compilation
-              if (info.minimized) {
-                return false
-              }
+            return true
+          })
+          .map(async (name) => {
+            const { info, source } = compilation.getAsset(name)
 
-              return true
-            })
-            .map(async (name) => {
-              const { info, source } = compilation.getAsset(name)
+            const eTag = cache.getLazyHashedEtag(source)
+            const output = await cache.getPromise(name, eTag)
 
-              const eTag = cache.getLazyHashedEtag(source)
-              const output = await cache.getPromise(name, eTag)
+            if (!output) {
+              numberOfAssetsForMinify += 1
+            }
 
-              if (!output) {
-                numberOfAssetsForMinify += 1
-              }
+            return { name, info, inputSource: source, output, eTag }
+          })
+      )
 
-              return { name, info, inputSource: source, output, eTag }
-            })
-        )
+      const numberOfWorkers = Math.min(
+        numberOfAssetsForMinify,
+        optimizeOptions.availableNumberOfCores
+      )
 
-        const numberOfWorkers = Math.min(
-          numberOfAssetsForMinify,
-          optimizeOptions.availableNumberOfCores
-        )
+      let initializedWorker
 
-        let initializedWorker
+      // eslint-disable-next-line consistent-return
+      const getWorker = () => {
+        if (initializedWorker) {
+          return initializedWorker
+        }
 
-        // eslint-disable-next-line consistent-return
-        const getWorker = () => {
-          if (initializedWorker) {
-            return initializedWorker
+        initializedWorker = new jestWorker(
+          path.join(__dirname, './minify.js'),
+          {
+            numWorkers: numberOfWorkers,
+            enableWorkerThreads: true,
           }
+        )
 
-          initializedWorker = new jestWorker(
-            path.join(__dirname, './minify.js'),
-            {
-              numWorkers: numberOfWorkers,
-              enableWorkerThreads: true,
-            }
-          )
+        initializedWorker.getStdout().pipe(process.stdout)
+        initializedWorker.getStderr().pipe(process.stderr)
 
-          initializedWorker.getStdout().pipe(process.stdout)
-          initializedWorker.getStderr().pipe(process.stderr)
+        return initializedWorker
+      }
 
-          return initializedWorker
-        }
+      const limit = pLimit(
+        numberOfAssetsForMinify > 0 ? numberOfWorkers : Infinity
+      )
+      const scheduledTasks = []
+
+      for (const asset of assetsForMinify) {
+        scheduledTasks.push(
+          limit(async () => {
+            const { name, inputSource, info, eTag } = asset
+            let { output } = asset
+
+            const minifySpan = terserSpan.traceChild('minify-fs')
+            minifySpan.setAttribute('name', name)
+            minifySpan.setAttribute(
+              'cache',
+              typeof output === 'undefined' ? 'MISS' : 'HIT'
+            )
 
-        const limit = pLimit(
-          numberOfAssetsForMinify > 0 ? numberOfWorkers : Infinity
-        )
-        const scheduledTasks = []
+            return minifySpan.traceAsyncFn(async () => {
+              if (!output) {
+                const {
+                  source: sourceFromInputSource,
+                  map: inputSourceMap,
+                } = inputSource.sourceAndMap()
 
-        for (const asset of assetsForMinify) {
-          scheduledTasks.push(
-            limit(async () => {
-              const { name, inputSource, info, eTag } = asset
-              let { output } = asset
+                const input = Buffer.isBuffer(sourceFromInputSource)
+                  ? sourceFromInputSource.toString()
+                  : sourceFromInputSource
 
-              const assetSpan = tracer.startSpan('minify-js', {
-                attributes: {
+                const options = {
                   name,
-                  cache: typeof output === 'undefined' ? 'MISS' : 'HIT',
-                },
-              })
-
-              return traceAsyncFn(assetSpan, async () => {
-                if (!output) {
-                  const {
-                    source: sourceFromInputSource,
-                    map: inputSourceMap,
-                  } = inputSource.sourceAndMap()
-
-                  const input = Buffer.isBuffer(sourceFromInputSource)
-                    ? sourceFromInputSource.toString()
-                    : sourceFromInputSource
-
-                  const options = {
-                    name,
-                    input,
-                    inputSourceMap,
-                    terserOptions: { ...this.options.terserOptions },
-                  }
+                  input,
+                  inputSourceMap,
+                  terserOptions: { ...this.options.terserOptions },
+                }
 
-                  if (typeof options.terserOptions.module === 'undefined') {
-                    if (typeof info.javascriptModule !== 'undefined') {
-                      options.terserOptions.module = info.javascriptModule
-                    } else if (/\.mjs(\?.*)?$/i.test(name)) {
-                      options.terserOptions.module = true
-                    } else if (/\.cjs(\?.*)?$/i.test(name)) {
-                      options.terserOptions.module = false
-                    }
+                if (typeof options.terserOptions.module === 'undefined') {
+                  if (typeof info.javascriptModule !== 'undefined') {
+                    options.terserOptions.module = info.javascriptModule
+                  } else if (/\.mjs(\?.*)?$/i.test(name)) {
+                    options.terserOptions.module = true
+                  } else if (/\.cjs(\?.*)?$/i.test(name)) {
+                    options.terserOptions.module = false
                   }
+                }
 
-                  try {
-                    output = await getWorker().minify(options)
-                  } catch (error) {
-                    compilation.errors.push(buildError(error, name))
+                try {
+                  output = await getWorker().minify(options)
+                } catch (error) {
+                  compilation.errors.push(buildError(error, name))
 
-                    return
-                  }
+                  return
+                }
 
-                  if (output.map) {
-                    output.source = new SourceMapSource(
-                      output.code,
-                      name,
-                      output.map,
-                      input,
-                      /** @type {SourceMapRawSourceMap} */ (inputSourceMap),
-                      true
-                    )
-                  } else {
-                    output.source = new RawSource(output.code)
-                  }
+                if (output.map) {
+                  output.source = new SourceMapSource(
+                    output.code,
+                    name,
+                    output.map,
+                    input,
+                    /** @type {SourceMapRawSourceMap} */ (inputSourceMap),
+                    true
+                  )
+                } else {
+                  output.source = new RawSource(output.code)
+                }
 
-                  if (isWebpack5) {
-                    await cache.storePromise(name, eTag, {
-                      source: output.source,
-                    })
-                  } else {
-                    await cache.storePromise(name, eTag, {
-                      code: output.code,
-                      map: output.map,
-                      name,
-                      input,
-                      inputSourceMap,
-                    })
-                  }
+                if (isWebpack5) {
+                  await cache.storePromise(name, eTag, {
+                    source: output.source,
+                  })
+                } else {
+                  await cache.storePromise(name, eTag, {
+                    code: output.code,
+                    map: output.map,
+                    name,
+                    input,
+                    inputSourceMap,
+                  })
                 }
+              }
 
-                /** @type {AssetInfo} */
-                const newInfo = { minimized: true }
-                const { source } = output
+              /** @type {AssetInfo} */
+              const newInfo = { minimized: true }
+              const { source } = output
 
-                compilation.updateAsset(name, source, newInfo)
-              })
+              compilation.updateAsset(name, source, newInfo)
             })
-          )
-        }
+          })
+        )
+      }
 
-        await Promise.all(scheduledTasks)
+      await Promise.all(scheduledTasks)
 
-        if (initializedWorker) {
-          await initializedWorker.end()
-        }
-      })
+      if (initializedWorker) {
+        await initializedWorker.end()
+      }
     })
   }
 
diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts
index 85b50ff02002..aaee1cfc66aa 100644
--- a/packages/next/export/index.ts
+++ b/packages/next/export/index.ts
@@ -46,8 +46,7 @@ import { PrerenderManifest } from '../build'
 import exportPage from './worker'
 import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
 import { getPagePath } from '../next-server/server/require'
-import { tracer, traceFn, traceAsyncFn } from '../build/tracer'
-import opentelemetryApi from '@opentelemetry/api'
+import { trace } from '../telemetry/trace'
 
 const exists = promisify(existsOrig)
 
@@ -137,20 +136,21 @@ export default async function exportApp(
   options: ExportOptions,
   configuration?: NextConfig
 ): Promise<void> {
-  const nextExportSpan = tracer.startSpan('next-export')
-  return traceAsyncFn(nextExportSpan, async () => {
+  const nextExportSpan = trace('next-export')
+
+  return nextExportSpan.traceAsyncFn(async () => {
     dir = resolve(dir)
 
     // attempt to load global env values so they are available in next.config.js
-    traceFn(tracer.startSpan('load-dotenv'), () =>
-      loadEnvConfig(dir, false, Log)
-    )
+    nextExportSpan
+      .traceChild('load-dotenv')
+      .traceFn(() => loadEnvConfig(dir, false, Log))
 
     const nextConfig =
       configuration ||
-      (await traceAsyncFn(tracer.startSpan('load-next-config'), () =>
-        loadConfig(PHASE_EXPORT, dir)
-      ))
+      (await nextExportSpan
+        .traceChild('load-next-config')
+        .traceAsyncFn(() => loadConfig(PHASE_EXPORT, dir)))
     const threads = options.threads || Math.max(cpus().length - 1, 1)
     const distDir = join(dir, nextConfig.distDir)
 
@@ -276,9 +276,11 @@ export default async function exportApp(
       if (!options.silent) {
         Log.info('Copying "static" directory')
       }
-      await traceAsyncFn(tracer.startSpan('copy-static-directory'), () =>
-        recursiveCopy(join(dir, 'static'), join(outDir, 'static'))
-      )
+      await nextExportSpan
+        .traceChild('copy-static-directory')
+        .traceAsyncFn(() =>
+          recursiveCopy(join(dir, 'static'), join(outDir, 'static'))
+        )
     }
 
     // Copy .next/static directory
@@ -289,12 +291,14 @@ export default async function exportApp(
       if (!options.silent) {
         Log.info('Copying "static build" directory')
       }
-      await traceAsyncFn(tracer.startSpan('copy-next-static-directory'), () =>
-        recursiveCopy(
-          join(distDir, CLIENT_STATIC_FILES_PATH),
-          join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
+      await nextExportSpan
+        .traceChild('copy-next-static-directory')
+        .traceAsyncFn(() =>
+          recursiveCopy(
+            join(distDir, CLIENT_STATIC_FILES_PATH),
+            join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
+          )
         )
-      )
     }
 
     // Get the exportPathMap from the config file
@@ -321,14 +325,14 @@ export default async function exportApp(
     }
 
     if (!options.buildExport) {
-      const { isNextImageImported } = await traceAsyncFn(
-        tracer.startSpan('is-next-image-imported'),
-        () =>
+      const { isNextImageImported } = await nextExportSpan
+        .traceChild('is-next-image-imported')
+        .traceAsyncFn(() =>
           promises
             .readFile(join(distDir, EXPORT_MARKER), 'utf8')
             .then((text) => JSON.parse(text))
             .catch(() => ({}))
-      )
+        )
 
       if (isNextImageImported && loader === 'default' && !hasNextSupport) {
         throw new Error(
@@ -376,9 +380,9 @@ export default async function exportApp(
     if (!options.silent && !options.buildExport) {
       Log.info(`Launching ${threads} workers`)
     }
-    const exportPathMap = await traceAsyncFn(
-      tracer.startSpan('run-export-path-map'),
-      () =>
+    const exportPathMap = await nextExportSpan
+      .traceChild('run-export-path-map')
+      .traceAsyncFn(() =>
         nextConfig.exportPathMap(defaultPathMap, {
           dev: false,
           dir,
@@ -386,7 +390,7 @@ export default async function exportApp(
           distDir,
           buildId,
         })
-    )
+      )
 
     if (
       !options.buildExport &&
@@ -483,14 +487,16 @@ export default async function exportApp(
       if (!options.silent) {
         Log.info('Copying "public" directory')
       }
-      await traceAsyncFn(tracer.startSpan('copy-public-directory'), () =>
-        recursiveCopy(publicDir, outDir, {
-          filter(path) {
-            // Exclude paths used by pages
-            return !exportPathMap[path]
-          },
-        })
-      )
+      await nextExportSpan
+        .traceChild('copy-public-directory')
+        .traceAsyncFn(() =>
+          recursiveCopy(publicDir, outDir, {
+            filter(path) {
+              // Exclude paths used by pages
+              return !exportPathMap[path]
+            },
+          })
+        )
     }
 
     const worker = new Worker(require.resolve('./worker'), {
@@ -508,17 +514,10 @@ export default async function exportApp(
 
     await Promise.all(
       filteredPaths.map(async (path) => {
-        const pageExportSpan = tracer.startSpan('export-page', {
-          attributes: { path },
-        })
-        return traceAsyncFn(pageExportSpan, async () => {
-          const spanContext = {}
-
-          opentelemetryApi.propagation.inject(
-            opentelemetryApi.context.active(),
-            spanContext
-          )
+        const pageExportSpan = nextExportSpan.traceChild('export-page')
+        pageExportSpan.setAttribute('path', path)
 
+        return pageExportSpan.traceAsyncFn(async () => {
           const result = await worker.default({
             path,
             pathMap: exportPathMap[path],
@@ -533,7 +532,7 @@ export default async function exportApp(
             optimizeFonts: nextConfig.experimental.optimizeFonts,
             optimizeImages: nextConfig.experimental.optimizeImages,
             optimizeCss: nextConfig.experimental.optimizeCss,
-            spanContext,
+            parentSpanId: pageExportSpan.id,
           })
 
           for (const validation of result.ampValidations || []) {
diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts
index ca2eaa5734db..d52d2d9102a3 100644
--- a/packages/next/export/worker.ts
+++ b/packages/next/export/worker.ts
@@ -16,8 +16,7 @@ import { GetStaticProps } from '../types'
 import { requireFontManifest } from '../next-server/server/require'
 import { FontManifest } from '../next-server/server/font-utils'
 import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'
-import { tracer, traceAsyncFn } from '../build/tracer'
-import opentelemetryApi from '@opentelemetry/api'
+import { trace } from '../telemetry/trace'
 
 const envConfig = require('../next-server/lib/runtime-config')
 
@@ -52,7 +51,7 @@ interface ExportPageInput {
   optimizeFonts: boolean
   optimizeImages: boolean
   optimizeCss: any
-  spanContext: any
+  parentSpanId: any
 }
 
 interface ExportPageResults {
@@ -85,7 +84,7 @@ type ComponentModule = ComponentType<{}> & {
 }
 
 export default async function exportPage({
-  spanContext,
+  parentSpanId,
   path,
   pathMap,
   distDir,
@@ -100,367 +99,357 @@ export default async function exportPage({
   optimizeImages,
   optimizeCss,
 }: ExportPageInput): Promise<ExportPageResults> {
-  return opentelemetryApi.context.with(
-    opentelemetryApi.propagation.extract(
-      opentelemetryApi.context.active(),
-      spanContext
-    ),
-    () => {
-      return traceAsyncFn(tracer.startSpan('export-page-worker'), async () => {
-        let results: ExportPageResults = {
-          ampValidations: [],
-        }
-
-        try {
-          const { query: originalQuery = {} } = pathMap
-          const { page } = pathMap
-          const filePath = normalizePagePath(path)
-          const isDynamic = isDynamicRoute(page)
-          const ampPath = `${filePath}.amp`
-          let renderAmpPath = ampPath
-          let query = { ...originalQuery }
-          let params: { [key: string]: string | string[] } | undefined
-
-          let updatedPath = (query.__nextSsgPath as string) || path
-          let locale = query.__nextLocale || renderOpts.locale
-          delete query.__nextLocale
-          delete query.__nextSsgPath
-
-          if (renderOpts.locale) {
-            const localePathResult = normalizeLocalePath(
-              path,
-              renderOpts.locales
-            )
-
-            if (localePathResult.detectedLocale) {
-              updatedPath = localePathResult.pathname
-              locale = localePathResult.detectedLocale
+  const exportPageSpan = trace('export-page-worker', parentSpanId)
 
-              if (locale === renderOpts.defaultLocale) {
-                renderAmpPath = `${normalizePagePath(updatedPath)}.amp`
-              }
-            }
-          }
+  return exportPageSpan.traceAsyncFn(async () => {
+    let results: ExportPageResults = {
+      ampValidations: [],
+    }
 
-          // We need to show a warning if they try to provide query values
-          // for an auto-exported page since they won't be available
-          const hasOrigQueryValues = Object.keys(originalQuery).length > 0
-          const queryWithAutoExportWarn = () => {
-            if (hasOrigQueryValues) {
-              throw new Error(
-                `\nError: you provided query values for ${path} which is an auto-exported page. These can not be applied since the page can no longer be re-rendered on the server. To disable auto-export for this page add \`getInitialProps\`\n`
-              )
-            }
+    try {
+      const { query: originalQuery = {} } = pathMap
+      const { page } = pathMap
+      const filePath = normalizePagePath(path)
+      const isDynamic = isDynamicRoute(page)
+      const ampPath = `${filePath}.amp`
+      let renderAmpPath = ampPath
+      let query = { ...originalQuery }
+      let params: { [key: string]: string | string[] } | undefined
+
+      let updatedPath = (query.__nextSsgPath as string) || path
+      let locale = query.__nextLocale || renderOpts.locale
+      delete query.__nextLocale
+      delete query.__nextSsgPath
+
+      if (renderOpts.locale) {
+        const localePathResult = normalizeLocalePath(path, renderOpts.locales)
+
+        if (localePathResult.detectedLocale) {
+          updatedPath = localePathResult.pathname
+          locale = localePathResult.detectedLocale
+
+          if (locale === renderOpts.defaultLocale) {
+            renderAmpPath = `${normalizePagePath(updatedPath)}.amp`
           }
-
-          // Check if the page is a specified dynamic route
-          if (isDynamic && page !== path) {
-            params =
-              getRouteMatcher(getRouteRegex(page))(updatedPath) || undefined
-            if (params) {
-              // we have to pass these separately for serverless
-              if (!serverless) {
-                query = {
-                  ...query,
-                  ...params,
-                }
-              }
-            } else {
-              throw new Error(
-                `The provided export path '${updatedPath}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch`
-              )
+        }
+      }
+
+      // We need to show a warning if they try to provide query values
+      // for an auto-exported page since they won't be available
+      const hasOrigQueryValues = Object.keys(originalQuery).length > 0
+      const queryWithAutoExportWarn = () => {
+        if (hasOrigQueryValues) {
+          throw new Error(
+            `\nError: you provided query values for ${path} which is an auto-exported page. These can not be applied since the page can no longer be re-rendered on the server. To disable auto-export for this page add \`getInitialProps\`\n`
+          )
+        }
+      }
+
+      // Check if the page is a specified dynamic route
+      if (isDynamic && page !== path) {
+        params = getRouteMatcher(getRouteRegex(page))(updatedPath) || undefined
+        if (params) {
+          // we have to pass these separately for serverless
+          if (!serverless) {
+            query = {
+              ...query,
+              ...params,
             }
           }
+        } else {
+          throw new Error(
+            `The provided export path '${updatedPath}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch`
+          )
+        }
+      }
+
+      const headerMocks = {
+        headers: {},
+        getHeader: () => ({}),
+        setHeader: () => {},
+        hasHeader: () => false,
+        removeHeader: () => {},
+        getHeaderNames: () => [],
+      }
+
+      const req = ({
+        url: updatedPath,
+        ...headerMocks,
+      } as unknown) as IncomingMessage
+      const res = ({
+        ...headerMocks,
+      } as unknown) as ServerResponse
+
+      if (path === '/500' && page === '/_error') {
+        res.statusCode = 500
+      }
+
+      envConfig.setConfig({
+        serverRuntimeConfig,
+        publicRuntimeConfig: renderOpts.runtimeConfig,
+      })
 
-          const headerMocks = {
-            headers: {},
-            getHeader: () => ({}),
-            setHeader: () => {},
-            hasHeader: () => false,
-            removeHeader: () => {},
-            getHeaderNames: () => [],
-          }
+      let htmlFilename = `${filePath}${sep}index.html`
+      if (!subFolders) htmlFilename = `${filePath}.html`
+
+      const pageExt = extname(page)
+      const pathExt = extname(path)
+      // Make sure page isn't a folder with a dot in the name e.g. `v1.2`
+      if (pageExt !== pathExt && pathExt !== '') {
+        // If the path has an extension, use that as the filename instead
+        htmlFilename = path
+      } else if (path === '/') {
+        // If the path is the root, just use index.html
+        htmlFilename = 'index.html'
+      }
+
+      const baseDir = join(outDir, dirname(htmlFilename))
+      let htmlFilepath = join(outDir, htmlFilename)
+
+      await promises.mkdir(baseDir, { recursive: true })
+      let html
+      let curRenderOpts: RenderOpts = {}
+      let renderMethod = renderToHTML
+
+      const renderedDuringBuild = (getStaticProps: any) => {
+        return !buildExport && getStaticProps && !isDynamicRoute(path)
+      }
+
+      if (serverless) {
+        const curUrl = url.parse(req.url!, true)
+        req.url = url.format({
+          ...curUrl,
+          query: {
+            ...curUrl.query,
+            ...query,
+          },
+        })
+        const { Component: mod, getServerSideProps } = await loadComponents(
+          distDir,
+          page,
+          serverless
+        )
+
+        if (getServerSideProps) {
+          throw new Error(
+            `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`
+          )
+        }
 
-          const req = ({
-            url: updatedPath,
-            ...headerMocks,
-          } as unknown) as IncomingMessage
-          const res = ({
-            ...headerMocks,
-          } as unknown) as ServerResponse
+        // if it was auto-exported the HTML is loaded here
+        if (typeof mod === 'string') {
+          html = mod
+          queryWithAutoExportWarn()
+        } else {
+          // for non-dynamic SSG pages we should have already
+          // prerendered the file
+          if (renderedDuringBuild((mod as ComponentModule).getStaticProps))
+            return results
 
-          if (path === '/500' && page === '/_error') {
-            res.statusCode = 500
+          if (
+            (mod as ComponentModule).getStaticProps &&
+            !htmlFilepath.endsWith('.html')
+          ) {
+            // make sure it ends with .html if the name contains a dot
+            htmlFilename += '.html'
+            htmlFilepath += '.html'
           }
 
-          envConfig.setConfig({
-            serverRuntimeConfig,
-            publicRuntimeConfig: renderOpts.runtimeConfig,
-          })
+          renderMethod = (mod as ComponentModule).renderReqToHTML
+          const result = await renderMethod(
+            req,
+            res,
+            'export',
+            {
+              ampPath: renderAmpPath,
+              /// @ts-ignore
+              optimizeFonts,
+              /// @ts-ignore
+              optimizeImages,
+              /// @ts-ignore
+              optimizeCss,
+              distDir,
+              fontManifest: optimizeFonts
+                ? requireFontManifest(distDir, serverless)
+                : null,
+              locale: locale!,
+              locales: renderOpts.locales!,
+            },
+            // @ts-ignore
+            params
+          )
+          curRenderOpts = (result as any).renderOpts || {}
+          html = (result as any).html
+        }
 
-          let htmlFilename = `${filePath}${sep}index.html`
-          if (!subFolders) htmlFilename = `${filePath}.html`
-
-          const pageExt = extname(page)
-          const pathExt = extname(path)
-          // Make sure page isn't a folder with a dot in the name e.g. `v1.2`
-          if (pageExt !== pathExt && pathExt !== '') {
-            // If the path has an extension, use that as the filename instead
-            htmlFilename = path
-          } else if (path === '/') {
-            // If the path is the root, just use index.html
-            htmlFilename = 'index.html'
-          }
+        if (!html && !(curRenderOpts as any).isNotFound) {
+          throw new Error(`Failed to render serverless page`)
+        }
+      } else {
+        const components = await loadComponents(distDir, page, serverless)
+
+        if (components.getServerSideProps) {
+          throw new Error(
+            `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`
+          )
+        }
 
-          const baseDir = join(outDir, dirname(htmlFilename))
-          let htmlFilepath = join(outDir, htmlFilename)
+        // for non-dynamic SSG pages we should have already
+        // prerendered the file
+        if (renderedDuringBuild(components.getStaticProps)) {
+          return results
+        }
 
-          await promises.mkdir(baseDir, { recursive: true })
-          let html
-          let curRenderOpts: RenderOpts = {}
-          let renderMethod = renderToHTML
+        // TODO: de-dupe the logic here between serverless and server mode
+        if (components.getStaticProps && !htmlFilepath.endsWith('.html')) {
+          // make sure it ends with .html if the name contains a dot
+          htmlFilepath += '.html'
+          htmlFilename += '.html'
+        }
 
-          const renderedDuringBuild = (getStaticProps: any) => {
-            return !buildExport && getStaticProps && !isDynamicRoute(path)
+        if (typeof components.Component === 'string') {
+          html = components.Component
+          queryWithAutoExportWarn()
+        } else {
+          /**
+           * This sets environment variable to be used at the time of static export by head.tsx.
+           * Using this from process.env allows targeting both serverless and SSR by calling
+           * `process.env.__NEXT_OPTIMIZE_FONTS`.
+           * TODO(prateekbh@): Remove this when experimental.optimizeFonts are being cleaned up.
+           */
+          if (optimizeFonts) {
+            process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
           }
+          if (optimizeImages) {
+            process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
+          }
+          if (optimizeCss) {
+            process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
+          }
+          curRenderOpts = {
+            ...components,
+            ...renderOpts,
+            ampPath: renderAmpPath,
+            params,
+            optimizeFonts,
+            optimizeImages,
+            optimizeCss,
+            fontManifest: optimizeFonts
+              ? requireFontManifest(distDir, serverless)
+              : null,
+            locale: locale as string,
+          }
+          // @ts-ignore
+          html = await renderMethod(req, res, page, query, curRenderOpts)
+        }
+      }
+      results.ssgNotFound = (curRenderOpts as any).isNotFound
+
+      const validateAmp = async (
+        rawAmpHtml: string,
+        ampPageName: string,
+        validatorPath?: string
+      ) => {
+        const validator = await AmpHtmlValidator.getInstance(validatorPath)
+        const result = validator.validateString(rawAmpHtml)
+        const errors = result.errors.filter((e) => e.severity === 'ERROR')
+        const warnings = result.errors.filter((e) => e.severity !== 'ERROR')
+
+        if (warnings.length || errors.length) {
+          results.ampValidations.push({
+            page: ampPageName,
+            result: {
+              errors,
+              warnings,
+            },
+          })
+        }
+      }
 
-          if (serverless) {
-            const curUrl = url.parse(req.url!, true)
-            req.url = url.format({
-              ...curUrl,
-              query: {
-                ...curUrl.query,
-                ...query,
-              },
-            })
-            const { Component: mod, getServerSideProps } = await loadComponents(
-              distDir,
-              page,
-              serverless
-            )
-
-            if (getServerSideProps) {
-              throw new Error(
-                `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`
-              )
-            }
+      if (curRenderOpts.inAmpMode && !curRenderOpts.ampSkipValidation) {
+        if (!results.ssgNotFound) {
+          await validateAmp(html, path, curRenderOpts.ampValidatorPath)
+        }
+      } else if (curRenderOpts.hybridAmp) {
+        // we need to render the AMP version
+        let ampHtmlFilename = `${ampPath}${sep}index.html`
+        if (!subFolders) {
+          ampHtmlFilename = `${ampPath}.html`
+        }
+        const ampBaseDir = join(outDir, dirname(ampHtmlFilename))
+        const ampHtmlFilepath = join(outDir, ampHtmlFilename)
 
-            // if it was auto-exported the HTML is loaded here
-            if (typeof mod === 'string') {
-              html = mod
-              queryWithAutoExportWarn()
-            } else {
-              // for non-dynamic SSG pages we should have already
-              // prerendered the file
-              if (renderedDuringBuild((mod as ComponentModule).getStaticProps))
-                return results
-
-              if (
-                (mod as ComponentModule).getStaticProps &&
-                !htmlFilepath.endsWith('.html')
-              ) {
-                // make sure it ends with .html if the name contains a dot
-                htmlFilename += '.html'
-                htmlFilepath += '.html'
-              }
-
-              renderMethod = (mod as ComponentModule).renderReqToHTML
-              const result = await renderMethod(
+        try {
+          await promises.access(ampHtmlFilepath)
+        } catch (_) {
+          // make sure it doesn't exist from manual mapping
+          let ampHtml
+          if (serverless) {
+            req.url += (req.url!.includes('?') ? '&' : '?') + 'amp=1'
+            // @ts-ignore
+            ampHtml = (
+              await (renderMethod as any)(
                 req,
                 res,
                 'export',
-                {
-                  ampPath: renderAmpPath,
-                  /// @ts-ignore
-                  optimizeFonts,
-                  /// @ts-ignore
-                  optimizeImages,
-                  /// @ts-ignore
-                  optimizeCss,
-                  distDir,
-                  fontManifest: optimizeFonts
-                    ? requireFontManifest(distDir, serverless)
-                    : null,
-                  locale: locale!,
-                  locales: renderOpts.locales!,
-                },
-                // @ts-ignore
+                curRenderOpts,
                 params
               )
-              curRenderOpts = (result as any).renderOpts || {}
-              html = (result as any).html
-            }
-
-            if (!html && !(curRenderOpts as any).isNotFound) {
-              throw new Error(`Failed to render serverless page`)
-            }
+            ).html
           } else {
-            const components = await loadComponents(distDir, page, serverless)
-
-            if (components.getServerSideProps) {
-              throw new Error(
-                `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}`
-              )
-            }
-
-            // for non-dynamic SSG pages we should have already
-            // prerendered the file
-            if (renderedDuringBuild(components.getStaticProps)) {
-              return results
-            }
-
-            // TODO: de-dupe the logic here between serverless and server mode
-            if (components.getStaticProps && !htmlFilepath.endsWith('.html')) {
-              // make sure it ends with .html if the name contains a dot
-              htmlFilepath += '.html'
-              htmlFilename += '.html'
-            }
-
-            if (typeof components.Component === 'string') {
-              html = components.Component
-              queryWithAutoExportWarn()
-            } else {
-              /**
-               * This sets environment variable to be used at the time of static export by head.tsx.
-               * Using this from process.env allows targeting both serverless and SSR by calling
-               * `process.env.__NEXT_OPTIMIZE_FONTS`.
-               * TODO(prateekbh@): Remove this when experimental.optimizeFonts are being cleaned up.
-               */
-              if (optimizeFonts) {
-                process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
-              }
-              if (optimizeImages) {
-                process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
-              }
-              if (optimizeCss) {
-                process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
-              }
-              curRenderOpts = {
-                ...components,
-                ...renderOpts,
-                ampPath: renderAmpPath,
-                params,
-                optimizeFonts,
-                optimizeImages,
-                optimizeCss,
-                fontManifest: optimizeFonts
-                  ? requireFontManifest(distDir, serverless)
-                  : null,
-                locale: locale as string,
-              }
+            ampHtml = await renderMethod(
+              req,
+              res,
+              page,
               // @ts-ignore
-              html = await renderMethod(req, res, page, query, curRenderOpts)
-            }
-          }
-          results.ssgNotFound = (curRenderOpts as any).isNotFound
-
-          const validateAmp = async (
-            rawAmpHtml: string,
-            ampPageName: string,
-            validatorPath?: string
-          ) => {
-            const validator = await AmpHtmlValidator.getInstance(validatorPath)
-            const result = validator.validateString(rawAmpHtml)
-            const errors = result.errors.filter((e) => e.severity === 'ERROR')
-            const warnings = result.errors.filter((e) => e.severity !== 'ERROR')
-
-            if (warnings.length || errors.length) {
-              results.ampValidations.push({
-                page: ampPageName,
-                result: {
-                  errors,
-                  warnings,
-                },
-              })
-            }
-          }
-
-          if (curRenderOpts.inAmpMode && !curRenderOpts.ampSkipValidation) {
-            if (!results.ssgNotFound) {
-              await validateAmp(html, path, curRenderOpts.ampValidatorPath)
-            }
-          } else if (curRenderOpts.hybridAmp) {
-            // we need to render the AMP version
-            let ampHtmlFilename = `${ampPath}${sep}index.html`
-            if (!subFolders) {
-              ampHtmlFilename = `${ampPath}.html`
-            }
-            const ampBaseDir = join(outDir, dirname(ampHtmlFilename))
-            const ampHtmlFilepath = join(outDir, ampHtmlFilename)
-
-            try {
-              await promises.access(ampHtmlFilepath)
-            } catch (_) {
-              // make sure it doesn't exist from manual mapping
-              let ampHtml
-              if (serverless) {
-                req.url += (req.url!.includes('?') ? '&' : '?') + 'amp=1'
-                // @ts-ignore
-                ampHtml = (
-                  await (renderMethod as any)(
-                    req,
-                    res,
-                    'export',
-                    curRenderOpts,
-                    params
-                  )
-                ).html
-              } else {
-                ampHtml = await renderMethod(
-                  req,
-                  res,
-                  page,
-                  // @ts-ignore
-                  { ...query, amp: '1' },
-                  curRenderOpts as any
-                )
-              }
-
-              if (!curRenderOpts.ampSkipValidation) {
-                await validateAmp(ampHtml, page + '?amp=1')
-              }
-              await promises.mkdir(ampBaseDir, { recursive: true })
-              await promises.writeFile(ampHtmlFilepath, ampHtml, 'utf8')
-            }
-          }
-
-          if ((curRenderOpts as any).pageData) {
-            const dataFile = join(
-              pagesDataDir,
-              htmlFilename.replace(/\.html$/, '.json')
-            )
-
-            await promises.mkdir(dirname(dataFile), { recursive: true })
-            await promises.writeFile(
-              dataFile,
-              JSON.stringify((curRenderOpts as any).pageData),
-              'utf8'
+              { ...query, amp: '1' },
+              curRenderOpts as any
             )
-
-            if (curRenderOpts.hybridAmp) {
-              await promises.writeFile(
-                dataFile.replace(/\.json$/, '.amp.json'),
-                JSON.stringify((curRenderOpts as any).pageData),
-                'utf8'
-              )
-            }
           }
-          results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate
 
-          if (results.ssgNotFound) {
-            // don't attempt writing to disk if getStaticProps returned not found
-            return results
+          if (!curRenderOpts.ampSkipValidation) {
+            await validateAmp(ampHtml, page + '?amp=1')
           }
-          await promises.writeFile(htmlFilepath, html, 'utf8')
-          return results
-        } catch (error) {
-          console.error(
-            `\nError occurred prerendering page "${path}". Read more: https://err.sh/next.js/prerender-error\n` +
-              error.stack
+          await promises.mkdir(ampBaseDir, { recursive: true })
+          await promises.writeFile(ampHtmlFilepath, ampHtml, 'utf8')
+        }
+      }
+
+      if ((curRenderOpts as any).pageData) {
+        const dataFile = join(
+          pagesDataDir,
+          htmlFilename.replace(/\.html$/, '.json')
+        )
+
+        await promises.mkdir(dirname(dataFile), { recursive: true })
+        await promises.writeFile(
+          dataFile,
+          JSON.stringify((curRenderOpts as any).pageData),
+          'utf8'
+        )
+
+        if (curRenderOpts.hybridAmp) {
+          await promises.writeFile(
+            dataFile.replace(/\.json$/, '.amp.json'),
+            JSON.stringify((curRenderOpts as any).pageData),
+            'utf8'
           )
-          return { ...results, error: true }
         }
-      })
+      }
+      results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate
+
+      if (results.ssgNotFound) {
+        // don't attempt writing to disk if getStaticProps returned not found
+        return results
+      }
+      await promises.writeFile(htmlFilepath, html, 'utf8')
+      return results
+    } catch (error) {
+      console.error(
+        `\nError occurred prerendering page "${path}". Read more: https://err.sh/next.js/prerender-error\n` +
+          error.stack
+      )
+      return { ...results, error: true }
     }
-  )
+  })
 }
diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts
index 684cf43e39fd..fae4b5020398 100644
--- a/packages/next/server/next-dev-server.ts
+++ b/packages/next/server/next-dev-server.ts
@@ -34,6 +34,7 @@ import { normalizePagePath } from '../next-server/server/normalize-page-path'
 import Router, { Params, route } from '../next-server/server/router'
 import { eventCliSession } from '../telemetry/events'
 import { Telemetry } from '../telemetry/storage'
+import { setGlobal } from '../telemetry/trace'
 import HotReloader from './hot-reloader'
 import { findPageFile } from './lib/find-page-file'
 import { getNodeOptionsWithoutInspect } from './lib/utils'
@@ -297,6 +298,8 @@ export default class DevServer extends Server {
         isCustomServer: this.isCustomServer,
       })
     )
+    // This is required by the tracing subsystem.
+    setGlobal('telemetry', telemetry)
   }
 
   protected async close(): Promise<void> {
diff --git a/packages/next/telemetry/trace/autoparent.ts b/packages/next/telemetry/trace/autoparent.ts
new file mode 100644
index 000000000000..e508488914e9
--- /dev/null
+++ b/packages/next/telemetry/trace/autoparent.ts
@@ -0,0 +1,71 @@
+import { trace, Span } from './trace'
+
+const stacks = new WeakMap<any, Array<Span>>()
+const stoppedSpansSets = new WeakMap<any, Set<Span>>()
+
+export function stackPush(keyObj: any, spanName: string, attrs?: any): Span {
+  let stack = stacks.get(keyObj)
+  let span
+
+  if (!stack) {
+    stack = []
+    stacks.set(keyObj, stack)
+    span = trace(spanName, undefined, attrs ? attrs() : undefined)
+  } else {
+    const parent = stack[stack.length - 1]
+    if (parent) {
+      span = trace(spanName, parent.id, attrs ? attrs() : undefined)
+    } else {
+      span = trace(spanName, undefined, attrs ? attrs() : undefined)
+    }
+  }
+
+  stack.push(span)
+  return span
+}
+
+export function stackPop(keyObj: any, span: any): void {
+  let stack = stacks.get(keyObj)
+  if (!stack) {
+    console.info(
+      'Attempted to pop from non-existent stack. Key reference must be bad.'
+    )
+    return
+  }
+
+  let stoppedSpans = stoppedSpansSets.get(keyObj)
+  if (!stoppedSpans) {
+    stoppedSpans = new Set()
+    stoppedSpansSets.set(keyObj, stoppedSpans)
+  }
+  if (stoppedSpans.has(span)) {
+    console.info(
+      `Attempted to terminate tracing span that was already stopped for ${span.name}`
+    )
+    return
+  }
+
+  while (true) {
+    let poppedSpan = stack.pop()
+
+    if (poppedSpan && poppedSpan === span) {
+      stoppedSpans.add(poppedSpan)
+      span.stop()
+      stoppedSpans.add(span)
+      break
+    } else if (poppedSpan === undefined || stack.indexOf(span) === -1) {
+      // We've either reached the top of the stack or the stack doesn't contain
+      // the span for another reason.
+      console.info(`Tracing span was not found in stack for: ${span.name}`)
+      stoppedSpans.add(span)
+      span.stop()
+      break
+    } else if (stack.indexOf(span) !== -1) {
+      console.info(
+        `Attempted to pop span that was not at top of stack for: ${span.name}`
+      )
+      stoppedSpans.add(poppedSpan)
+      poppedSpan.stop()
+    }
+  }
+}
diff --git a/packages/next/telemetry/trace/index.ts b/packages/next/telemetry/trace/index.ts
new file mode 100644
index 000000000000..4f03fdcaa5bb
--- /dev/null
+++ b/packages/next/telemetry/trace/index.ts
@@ -0,0 +1,5 @@
+import { trace, Span, SpanStatus } from './trace'
+import { SpanId, setGlobal } from './shared'
+import { stackPush, stackPop } from './autoparent'
+
+export { trace, SpanId, Span, SpanStatus, stackPush, stackPop, setGlobal }
diff --git a/packages/next/telemetry/trace/report/index.ts b/packages/next/telemetry/trace/report/index.ts
new file mode 100644
index 000000000000..2f8130cab963
--- /dev/null
+++ b/packages/next/telemetry/trace/report/index.ts
@@ -0,0 +1,33 @@
+import { TARGET, SpanId } from '../shared'
+import reportToConsole from './to-console'
+import reportToZipkin from './to-zipkin'
+import reportToTelemetry from './to-telemetry'
+
+type Reporter = (
+  spanName: string,
+  duration: number,
+  timestamp: number,
+  id: SpanId,
+  parentId?: SpanId,
+  attrs?: Object
+) => void
+
+const target =
+  process.env.TRACE_TARGET && process.env.TRACE_TARGET in TARGET
+    ? TARGET[process.env.TRACE_TARGET as TARGET]
+    : TARGET.TELEMETRY
+
+if (process.env.TRACE_TARGET && !target) {
+  console.info(
+    'For TRACE_TARGET, please specify one of: CONSOLE, ZIPKIN, TELEMETRY'
+  )
+}
+
+export let report: Reporter
+if (target === TARGET.CONSOLE) {
+  report = reportToConsole
+} else if (target === TARGET.ZIPKIN) {
+  report = reportToZipkin
+} else {
+  report = reportToTelemetry
+}
diff --git a/packages/next/telemetry/trace/report/to-console.ts b/packages/next/telemetry/trace/report/to-console.ts
new file mode 100644
index 000000000000..1f53bfd53acf
--- /dev/null
+++ b/packages/next/telemetry/trace/report/to-console.ts
@@ -0,0 +1,26 @@
+const idToName = new Map<string, string>()
+
+const reportToConsole = (
+  spanName: string,
+  duration: number,
+  _timestamp: number,
+  id: string,
+  parentId?: string,
+  attrs?: Object
+) => {
+  idToName.set(id, spanName)
+
+  const parentStr =
+    parentId && idToName.has(parentId)
+      ? `, parent: ${idToName.get(parentId)}`
+      : ''
+  const attrsStr = attrs
+    ? `, ${Object.entries(attrs)
+        .map(([key, val]) => `${key}: ${val}`)
+        .join(', ')}`
+    : ''
+
+  console.log(`[trace] ${spanName} took ${duration} μs${parentStr}${attrsStr}`)
+}
+
+export default reportToConsole
diff --git a/packages/next/telemetry/trace/report/to-telemetry.ts b/packages/next/telemetry/trace/report/to-telemetry.ts
new file mode 100644
index 000000000000..51d46ca59637
--- /dev/null
+++ b/packages/next/telemetry/trace/report/to-telemetry.ts
@@ -0,0 +1,27 @@
+import { traceGlobals } from '../shared'
+
+const TRACE_EVENT_WHITELIST = new Map(
+  Object.entries({
+    'webpack-invalidated': 'WEBPACK_INVALIDATED',
+  })
+)
+
+const reportToTelemetry = (spanName: string, duration: number) => {
+  const eventName = TRACE_EVENT_WHITELIST.get(spanName)
+  if (!eventName) {
+    return
+  }
+  const telemetry = traceGlobals.get('telemetry')
+  if (!telemetry) {
+    return
+  }
+
+  telemetry.record({
+    eventName,
+    payload: {
+      durationInMicroseconds: duration,
+    },
+  })
+}
+
+export default reportToTelemetry
diff --git a/packages/next/telemetry/trace/report/to-zipkin.ts b/packages/next/telemetry/trace/report/to-zipkin.ts
new file mode 100644
index 000000000000..c65ab8279272
--- /dev/null
+++ b/packages/next/telemetry/trace/report/to-zipkin.ts
@@ -0,0 +1,45 @@
+import { randomBytes } from 'crypto'
+import fetch from 'node-fetch'
+
+let traceId = process.env.TRACE_ID
+if (!traceId) {
+  traceId = process.env.TRACE_ID = randomBytes(8).toString('hex')
+}
+
+const localEndpoint = {
+  serviceName: 'zipkin-query',
+  ipv4: '127.0.0.1',
+  port: 9411,
+}
+const zipkinUrl = `http://${localEndpoint.ipv4}:${localEndpoint.port}/api/v2/spans`
+
+const reportToLocalHost = (
+  name: string,
+  duration: number,
+  timestamp: number,
+  id: string,
+  parentId?: string,
+  attrs?: Object
+) => {
+  const body = [
+    {
+      traceId,
+      parentId,
+      name,
+      id,
+      timestamp,
+      duration,
+      localEndpoint,
+      tags: attrs,
+    },
+  ]
+
+  // We intentionally do not block on I/O here.
+  fetch(zipkinUrl, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(body),
+  }).catch(() => {})
+}
+
+export default reportToLocalHost
diff --git a/packages/next/telemetry/trace/shared.ts b/packages/next/telemetry/trace/shared.ts
new file mode 100644
index 000000000000..73aa00e2af36
--- /dev/null
+++ b/packages/next/telemetry/trace/shared.ts
@@ -0,0 +1,12 @@
+export enum TARGET {
+  CONSOLE = 'CONSOLE',
+  ZIPKIN = 'ZIPKIN',
+  TELEMETRY = 'TELEMETRY',
+}
+
+export type SpanId = string
+
+export const traceGlobals: Map<any, any> = new Map()
+export const setGlobal = (key: any, val: any) => {
+  traceGlobals.set(key, val)
+}
diff --git a/packages/next/telemetry/trace/trace.ts b/packages/next/telemetry/trace/trace.ts
new file mode 100644
index 000000000000..24a3eda1136d
--- /dev/null
+++ b/packages/next/telemetry/trace/trace.ts
@@ -0,0 +1,83 @@
+import { randomBytes } from 'crypto'
+import { SpanId } from './shared'
+import { report } from './report'
+
+const NUM_OF_MICROSEC_IN_SEC = BigInt('1000')
+
+const getId = () => randomBytes(8).toString('hex')
+
+export enum SpanStatus {
+  Started,
+  Stopped,
+}
+
+export class Span {
+  name: string
+  id: SpanId
+  parentId?: SpanId
+  duration: number | null
+  attrs: { [key: string]: any }
+  status: SpanStatus
+
+  _start: bigint
+
+  constructor(name: string, parentId?: SpanId, attrs?: Object) {
+    this.name = name
+    this.parentId = parentId
+    this.duration = null
+    this.attrs = attrs ? { ...attrs } : {}
+    this.status = SpanStatus.Started
+    this.id = getId()
+    this._start = process.hrtime.bigint()
+  }
+
+  // Durations are reported as microseconds. This gives 1000x the precision
+  // of something like Date.now(), which reports in milliseconds.
+  // Additionally, ~285 years can be safely represented as microseconds as
+  // a float64 in both JSON and JavaScript.
+  stop() {
+    const end: bigint = process.hrtime.bigint()
+    const duration = (end - this._start) / NUM_OF_MICROSEC_IN_SEC
+    this.status = SpanStatus.Stopped
+    if (duration > Number.MAX_SAFE_INTEGER) {
+      throw new Error(`Duration is too long to express as float64: ${duration}`)
+    }
+    const timestamp = this._start / NUM_OF_MICROSEC_IN_SEC
+    report(
+      this.name,
+      Number(duration),
+      Number(timestamp),
+      this.id,
+      this.parentId,
+      this.attrs
+    )
+  }
+
+  traceChild(name: string, attrs?: Object) {
+    return new Span(name, this.id, attrs)
+  }
+
+  setAttribute(key: string, value: any) {
+    this.attrs[key] = value
+  }
+
+  traceFn(fn: any) {
+    try {
+      return fn()
+    } finally {
+      this.stop()
+    }
+  }
+
+  async traceAsyncFn(fn: any) {
+    try {
+      return await fn()
+    } finally {
+      this.stop()
+    }
+  }
+}
+
+export const trace = (name: string, parentId?: SpanId, attrs?: Object) => {
+  return new Span(name, parentId, attrs)
+}
diff --git a/yarn.lock b/yarn.lock
index 5423dfc08ee6..0e99788b1106 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2488,103 +2488,18 @@
     "@octokit/openapi-types" "^4.0.2"
     "@types/node" ">= 8"
 
-"@opentelemetry/api@0.14.0", "@opentelemetry/api@^0.14.0":
+"@opentelemetry/api@0.14.0":
   version "0.14.0"
   resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.14.0.tgz#4e17d8d2f1da72b19374efa7b6526aa001267cae"
   integrity sha512-L7RMuZr5LzMmZiQSQDy9O1jo0q+DaLy6XpYJfIGfYSfoJA5qzYwUP3sP1uMIQ549DvxAgM3ng85EaPTM/hUHwQ==
   dependencies:
     "@opentelemetry/context-base" "^0.14.0"
 
-"@opentelemetry/context-async-hooks@^0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-0.14.0.tgz#34b77e87060bea2489b16f55168c35a580c5de50"
-  integrity sha512-BgAPWbmelvL/xhI0B17aidyw24E1D69PlKjDZD1L5AI050Glg1+pa6W0jMSoZltnM27Bc+Pc9n22fqxa4kvLRg==
-  dependencies:
-    "@opentelemetry/context-base" "^0.14.0"
-
 "@opentelemetry/context-base@^0.14.0":
   version "0.14.0"
   resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001"
   integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw==
 
-"@opentelemetry/core@^0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-0.14.0.tgz#35870e58e3d084dfc4dea99c973f3cf7479b3c6a"
-  integrity sha512-HJ4VM0cV6c5qjdW7C7koB2IT4ADunCOehxnKFRslQkbDqAEA1w42AZ9679siYALpWYxNqcJyqF2jxCNtfNHa6Q==
-  dependencies:
-    "@opentelemetry/api" "^0.14.0"
-    "@opentelemetry/context-base" "^0.14.0"
-    semver "^7.1.3"
-
-"@opentelemetry/exporter-zipkin@0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-0.14.0.tgz#35281d96bd101c0c1a48837868ba8249a0332f7f"
-  integrity sha512-cg/86iRK64NSGEcWOTzTfqMByyAu8udi3HgcYP2r0WF8JseabfhrlnBp/4/ba4HuoaGe8uXVRmvDT0uAguhO8w==
-  dependencies:
-    "@opentelemetry/api" "^0.14.0"
-    "@opentelemetry/core" "^0.14.0"
-    "@opentelemetry/resources" "^0.14.0"
-    "@opentelemetry/tracing" "^0.14.0"
-
-"@opentelemetry/node@0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/node/-/node-0.14.0.tgz#20af865de191a6b14443d619bb08d49003e64b19"
-  integrity sha512-79egldZKRIqXArjLHD8SYTJUfY5PrcFZll4+ykfbz23vy/CXEVev+1S8bS8bkE08DUNzskDuiFYlLdWTY8fdrQ==
-  dependencies:
-    "@opentelemetry/api" "^0.14.0"
-    "@opentelemetry/context-async-hooks" "^0.14.0"
-    "@opentelemetry/core" "^0.14.0"
-    "@opentelemetry/tracing" "^0.14.0"
-    require-in-the-middle "^5.0.0"
-    semver "^7.1.3"
-
-"@opentelemetry/plugin-http@0.14.0", "@opentelemetry/plugin-http@^0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/plugin-http/-/plugin-http-0.14.0.tgz#5c5ac23bd3a5cbd8891fce857d52a86bb0dbf51c"
-  integrity sha512-j6VHSotl+geN+znYgGgn/GohTAirAtHum8Zpc52VZ/WQCrNfHcmEf4NYaGCuz5tW2EgmbH+66iDM/lgNadQuQg==
-  dependencies:
-    "@opentelemetry/api" "^0.14.0"
-    "@opentelemetry/core" "^0.14.0"
-    "@opentelemetry/semantic-conventions" "^0.14.0"
-    semver "^7.1.3"
-    shimmer "^1.2.1"
-
-"@opentelemetry/plugin-https@0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/plugin-https/-/plugin-https-0.14.0.tgz#60a0db56339de497b35aaae0de91b10ecff9b0ea"
-  integrity sha512-FyT3iaEO9CdGu1EeIe+JmAwfoYwjG9GtHiyWSovFEKO/0OLQLrXA2AQDgltOeelu/snPGB2MF92iPqbpfVdhHw==
-  dependencies:
-    "@opentelemetry/api" "^0.14.0"
-    "@opentelemetry/core" "^0.14.0"
-    "@opentelemetry/plugin-http" "^0.14.0"
-    "@opentelemetry/semantic-conventions" "^0.14.0"
-    semver "^7.1.3"
-    shimmer "^1.2.1"
-
-"@opentelemetry/resources@^0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-0.14.0.tgz#e89378931b4e02d4b6fb526d237d9594db2bb68a"
-  integrity sha512-7XVML4HxvoH6kWY+x0mhMc5m0a2YBvPCTSX7yAqyp9XIGvFpdjzAE2ggJ40DZrL1sPv9f0QYAbnIKFDVLBTfGA==
-  dependencies:
-    "@opentelemetry/api" "^0.14.0"
-    "@opentelemetry/core" "^0.14.0"
-
-"@opentelemetry/semantic-conventions@^0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-0.14.0.tgz#7b48f4300709b34cef0beed8099b79e4a5907886"
-  integrity sha512-iDGRLQwo+ka1ljlLo4KyuUmzsJwtPw+PyCjetQwn3m/pTXjdyWLGOTARBrpQGpkQp7k87RaCCg5AqZaKFU2G6g==
-
-"@opentelemetry/tracing@0.14.0", "@opentelemetry/tracing@^0.14.0":
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/tracing/-/tracing-0.14.0.tgz#f1e528b90def213a454ca410ca2d460acfacbe3f"
-  integrity sha512-sw9lXJQUQO0xaCvHWFUzIhgh2jGFgXubRQ6g1O84Q/ILU93ZMMVt+d97mihcrtBrV89Sy39HF8tAwekjpzv+cA==
-  dependencies:
-    "@opentelemetry/api" "^0.14.0"
-    "@opentelemetry/context-base" "^0.14.0"
-    "@opentelemetry/core" "^0.14.0"
-    "@opentelemetry/resources" "^0.14.0"
-    "@opentelemetry/semantic-conventions" "^0.14.0"
-
 "@polka/url@^1.0.0-next.9":
   version "1.0.0-next.11"
   resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71"
@@ -11071,10 +10986,6 @@ modify-values@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
 
-module-details-from-path@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b"
-
 moment@^2.24.0:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -13990,15 +13901,6 @@ require-from-string@^1.1.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
 
-require-in-the-middle@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.1.0.tgz#b768f800377b47526d026bbf5a7f727f16eb412f"
-  integrity sha512-M2rLKVupQfJ5lf9OvqFGIT+9iVLnTmjgbOmpil12hiSQNn5zJTKGPoIisETNjfK+09vP3rpm1zJajmErpr2sEQ==
-  dependencies:
-    debug "^4.1.1"
-    module-details-from-path "^1.0.3"
-    resolve "^1.12.0"
-
 require-like@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa"
@@ -14614,11 +14516,6 @@ shellwords@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
 
-shimmer@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
-  integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
-
 side-channel@^1.0.2, side-channel@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
