Repository: vercel/next.js
Pull request: Telemetry: track failed builds and build bundler usage

This:

- Tracks build failures as a separate event, `NEXT_BUILD_FAILED`
- Added `bundler` property to both `NEXT_BUILD_COMPLETED` and `NEXT_BUILD_FAILED`
- Requires a server-side update

Alternatives considered:

Considered adding a `success: boolean` property to the existing `EventBuildCompleted`. There are a few issues with this:

- Expands the semantics of the existing event. This may have unintended consequences if we don’t change how we report on these events to filter success state
- This event requires page paths and timing information from the compiler, neither of which are guaranteed to exist on failures

Test Plan:
- Added a syntax error to a module and built with `NEXT_TELEMETRY_DEBUG=1`. Verified the failure event is sent along with the bundler value
- Verified bundler values are now sent for webpack, rspack, and Turbopack on build successes


Closes PACK-4301

Changed files:
filename: packages/next/src/build/index.ts
patch: @@ -107,6 +107,7 @@ import {
   EVENT_BUILD_FEATURE_USAGE,
   eventPackageUsedInGetServerSideProps,
   eventBuildCompleted,
+  eventBuildFailed,
 } from '../telemetry/events'
 import type { EventBuildFeatureUsage } from '../telemetry/events'
 import { Telemetry } from '../telemetry/storage'
@@ -210,6 +211,8 @@ import { isPersistentCachingEnabled } from '../shared/lib/turbopack/utils'
 import { inlineStaticEnv } from '../lib/inline-static-env'
 import { populateStaticEnv } from '../lib/static-env'
 import { durationToString } from './duration-to-string'
+import { traceGlobals } from '../trace/shared'
+import { extractNextErrorCode } from '../lib/error-telemetry-utils'
 
 type Fallback = null | boolean | string
 
@@ -826,6 +829,7 @@ export default async function build(
   const isCompileMode = experimentalBuildMode === 'compile'
   const isGenerateMode = experimentalBuildMode === 'generate'
   NextBuildContext.isCompileMode = isCompileMode
+  const buildStartTime = Date.now()
 
   let loadedConfig: NextConfigComplete | undefined
   try {
@@ -1471,6 +1475,7 @@ export default async function build(
 
           telemetry.record(
             eventBuildCompleted(pagesPaths, {
+              bundler: 'turbopack',
               durationInSeconds: Math.round(compilerDuration),
               totalAppPagesCount,
             })
@@ -1558,6 +1563,7 @@ export default async function build(
 
             telemetry.record(
               eventBuildCompleted(pagesPaths, {
+                bundler: getBundlerForTelemetry(isTurbopack),
                 durationInSeconds,
                 totalAppPagesCount,
               })
@@ -1573,6 +1579,7 @@ export default async function build(
 
             telemetry.record(
               eventBuildCompleted(pagesPaths, {
+                bundler: getBundlerForTelemetry(isTurbopack),
                 durationInSeconds: compilerDuration,
                 totalAppPagesCount,
               })
@@ -3705,6 +3712,18 @@ export default async function build(
 
       await shutdownPromise
     })
+  } catch (e) {
+    const telemetry: Telemetry | undefined = traceGlobals.get('telemetry')
+    if (telemetry) {
+      telemetry.record(
+        eventBuildFailed({
+          bundler: getBundlerForTelemetry(isTurbopack),
+          errorCode: getErrorCodeForTelemetry(e),
+          durationInSeconds: Math.floor((Date.now() - buildStartTime) / 1000),
+        })
+      )
+    }
+    throw e
   } finally {
     // Ensure we wait for lockfile patching if present
     await lockfilePatchPromise.cur
@@ -3751,3 +3770,32 @@ function warnAboutTurbopackBuilds(config?: NextConfigComplete) {
 
   Log.warn(warningStr)
 }
+
+function getBundlerForTelemetry(isTurbopack: boolean) {
+  if (isTurbopack) {
+    return 'turbopack'
+  }
+
+  if (process.env.NEXT_RSPACK) {
+    return 'rspack'
+  }
+
+  return 'webpack'
+}
+
+function getErrorCodeForTelemetry(err: unknown) {
+  const code = extractNextErrorCode(err)
+  if (code != null) {
+    return code
+  }
+
+  if (err instanceof Error && 'code' in err && typeof err.code === 'string') {
+    return err.code
+  }
+
+  if (err instanceof Error) {
+    return err.name
+  }
+
+  return 'Unknown'
+}

filename: packages/next/src/telemetry/events/build.ts
patch: @@ -54,6 +54,7 @@ export function eventLintCheckCompleted(event: EventLintCheckCompleted): {
 
 const EVENT_BUILD_COMPLETED = 'NEXT_BUILD_COMPLETED'
 type EventBuildCompleted = {
+  bundler: 'webpack' | 'rspack' | 'turbopack'
   durationInSeconds: number
   totalPageCount: number
   hasDunderPages: boolean
@@ -85,6 +86,20 @@ export function eventBuildCompleted(
   }
 }
 
+const EVENT_BUILD_FAILED = 'NEXT_BUILD_FAILED'
+type EventBuildFailed = {
+  bundler: 'webpack' | 'rspack' | 'turbopack'
+  errorCode: string
+  durationInSeconds: number
+}
+
+export function eventBuildFailed(event: EventBuildFailed) {
+  return {
+    eventName: EVENT_BUILD_FAILED,
+    payload: event,
+  }
+}
+
 const EVENT_BUILD_OPTIMIZED = 'NEXT_BUILD_OPTIMIZED'
 type EventBuildOptimized = {
   durationInSeconds: number