diff --git a/MIGRATION.md b/MIGRATION.md index 861e092d7dbb..814446198405 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1156,6 +1156,8 @@ Note that v10.30.0 deprecated a top-level `reactComponentAnnotation` in favour o On Turbopack, component annotation requires Next.js 16+. The SDK now warns at build time if annotation is enabled on an older Next.js version, where it previously did nothing silently. +**Default `environment` on Vercel no longer has a `vercel-` prefix:** On Vercel, the SDK now defaults `environment` to the value of `VERCEL_TARGET_ENV` (`production`, `preview`, or a custom environment name) instead of `vercel-production` / `vercel-preview`. Update alert rules, dashboards and saved searches that reference the old names, or keep them by setting `environment` explicitly. + **Vercel AI no longer supported on Edge runtime:** We now rely on diagnostics channels for our Vercel AI instrumentation, which does not work on the Edge runtime. Because of this, monitoring of the `ai` package is no longer supported on Edge. Note that Edge is deprecated. ### Cloudflare: `nodejs_compat` compatibility flag is now required @@ -1689,6 +1691,10 @@ moved under the `webpack` option in v10; use the replacement listed below instea The deprecated `sourceMapsUploadOptions` and other deprecated Vite/build plugin options were removed from `@sentry/astro`, `@sentry/nuxt` and `@sentry/sveltekit`. Use the top-level equivalents (e.g. `sourcemaps`, `release`, `authToken`, `org`, `project`, `telemetry`) instead. +### Bundler plugins: Vercel deploys use the plain Vercel environment name + +Deploys that the bundler plugins create automatically on Vercel now use the value of `VERCEL_TARGET_ENV` (`production`, `preview`, or a custom environment name) as their environment instead of `vercel-production` / `vercel-preview`. This matches the new default runtime `environment` of `@sentry/nextjs`, and the `production` default of all other SDKs. If your events use a different environment, set `release.deploy.env` to the same value, or set `release.deploy` to `false` to opt out. + ### Removed `unstable_` bundler plugin options The `unstable_sentry*PluginOptions` escape hatch was removed from every SDK. It existed because the Sentry diff --git a/packages/bundler-plugins/src/core/options-mapping.ts b/packages/bundler-plugins/src/core/options-mapping.ts index 42629509a737..90de13daa8c9 100644 --- a/packages/bundler-plugins/src/core/options-mapping.ts +++ b/packages/bundler-plugins/src/core/options-mapping.ts @@ -1,3 +1,4 @@ +import { getVercelEnv } from '@sentry/core'; import type { Logger } from './logger'; import type { Options as UserOptions, @@ -162,9 +163,10 @@ export function normalizeUserOptions(userOptions: UserOptions): NormalizedOption } } - if (options.release.deploy === undefined && process.env['VERCEL'] && process.env['VERCEL_TARGET_ENV']) { + const vercelEnv = getVercelEnv(); + if (options.release.deploy === undefined && process.env['VERCEL'] && vercelEnv) { options.release.deploy = { - env: `vercel-${process.env['VERCEL_TARGET_ENV']}`, + env: vercelEnv, url: process.env['VERCEL_URL'] ? `https://${process.env['VERCEL_URL']}` : undefined, }; } diff --git a/packages/bundler-plugins/test/core/option-mappings.test.ts b/packages/bundler-plugins/test/core/option-mappings.test.ts index e1976d146c37..08998f525fc7 100644 --- a/packages/bundler-plugins/test/core/option-mappings.test.ts +++ b/packages/bundler-plugins/test/core/option-mappings.test.ts @@ -139,7 +139,7 @@ describe('normalizeUserOptions()', () => { const normalizedOptions = normalizeUserOptions(userOptions); expect(normalizedOptions.release.deploy).toEqual({ - env: 'vercel-production', + env: 'production', url: 'https://my-app.vercel.app', }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dc01c23fda8f..0eef1ba9f64c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -81,6 +81,7 @@ export { filterCookies as _INTERNAL_filterCookies } from './utils/data-collectio export { filterQueryParams as _INTERNAL_filterQueryParams } from './utils/data-collection/filterQueryParams'; export { filterCollectedUrl, filterCollectedUrlQuery } from './utils/data-collection/filterCollectedUrl'; export { envToBool } from './utils/envToBool'; +export { getVercelEnv } from './utils/vercel'; export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData'; export { prepareEvent } from './utils/prepareEvent'; export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; diff --git a/packages/core/src/utils/vercel.ts b/packages/core/src/utils/vercel.ts new file mode 100644 index 000000000000..ea4c64829fbb --- /dev/null +++ b/packages/core/src/utils/vercel.ts @@ -0,0 +1,15 @@ +/** + * Returns the Vercel environment the code is running in, determined by the `VERCEL_TARGET_ENV` environment variable + * with `VERCEL_ENV` as fallback. Returns `undefined` outside of Vercel. + * + * `VERCEL_TARGET_ENV` also carries the name of custom Vercel environments, whereas `VERCEL_ENV` only ever holds + * `production`, `preview` or `development`. SDKs use this as the default `environment` and the bundler plugins use it + * for the deploys they create, so the two stay in sync and deploys show up next to events. + */ +export function getVercelEnv(): string | undefined { + if (typeof process === 'undefined') { + return undefined; + } + + return process.env.VERCEL_TARGET_ENV || process.env.VERCEL_ENV || undefined; +} diff --git a/packages/core/test/lib/utils/vercel.test.ts b/packages/core/test/lib/utils/vercel.test.ts new file mode 100644 index 000000000000..0505e60dc7e6 --- /dev/null +++ b/packages/core/test/lib/utils/vercel.test.ts @@ -0,0 +1,33 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getVercelEnv } from '../../../src/utils/vercel'; + +describe('getVercelEnv', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + delete process.env.VERCEL_ENV; + delete process.env.VERCEL_TARGET_ENV; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('returns undefined when no Vercel env vars are set', () => { + expect(getVercelEnv()).toBeUndefined(); + }); + + it('returns VERCEL_TARGET_ENV without a prefix', () => { + process.env.VERCEL_ENV = 'preview'; + process.env.VERCEL_TARGET_ENV = 'staging'; + + expect(getVercelEnv()).toBe('staging'); + }); + + it('falls back to VERCEL_ENV when VERCEL_TARGET_ENV is not set', () => { + process.env.VERCEL_ENV = 'production'; + + expect(getVercelEnv()).toBe('production'); + }); +}); diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 5c5d3ffc2c85..abdba2d67d7d 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -7,7 +7,7 @@ import type { BrowserOptions } from '@sentry/react'; import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; -import { getVercelEnv } from '../common/getVercelEnv'; +import { getClientVercelEnv } from '../common/getVercelEnv'; import { isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import { browserTracingIntegration } from './browserTracingIntegration'; import { nextjsClientStackFrameNormalizationIntegration } from './clientNormalizationIntegration'; @@ -63,7 +63,7 @@ export function init(options: BrowserOptions): Client | undefined { } const opts = { - environment: options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(true) || process.env.NODE_ENV, + environment: options.environment || process.env.SENTRY_ENVIRONMENT || getClientVercelEnv() || process.env.NODE_ENV, defaultIntegrations: getDefaultIntegrations(options), release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, ...options, diff --git a/packages/nextjs/src/common/getVercelEnv.ts b/packages/nextjs/src/common/getVercelEnv.ts index 98755c91409d..cf977f0ee3cb 100644 --- a/packages/nextjs/src/common/getVercelEnv.ts +++ b/packages/nextjs/src/common/getVercelEnv.ts @@ -1,9 +1,7 @@ /** - * Returns an environment setting value determined by Vercel's `VERCEL_ENV` environment variable. - * - * @param isClient Flag to indicate whether to use the `NEXT_PUBLIC_` prefixed version of the environment variable. + * Browser counterpart of `getVercelEnv` from `@sentry/core`, reading the `NEXT_PUBLIC_` variants that Next.js exposes + * to the client. The variables must be referenced statically so Next.js can inline them at build time. */ -export function getVercelEnv(isClient: boolean): string | undefined { - const vercelEnvVar = isClient ? process.env.NEXT_PUBLIC_VERCEL_ENV : process.env.VERCEL_ENV; - return vercelEnvVar ? `vercel-${vercelEnvVar}` : undefined; +export function getClientVercelEnv(): string | undefined { + return process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV || undefined; } diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 33d417507b39..9765260239a4 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -3,12 +3,19 @@ /* eslint-disable import/export */ import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes'; import type { EventProcessor } from '@sentry/core'; -import { applySdkMetadata, debug, getClient, getGlobalScope, getRootSpan, GLOBAL_OBJ } from '@sentry/core'; +import { + applySdkMetadata, + debug, + getClient, + getGlobalScope, + getRootSpan, + getVercelEnv, + GLOBAL_OBJ, +} from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; -import { getVercelEnv } from '../common/getVercelEnv'; import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { isBuild } from '../common/utils/isBuild'; @@ -138,7 +145,7 @@ export function init(options: NodeOptions): NodeClient | undefined { const cloudflareConfig = getCloudflareRuntimeConfig(); const opts: NodeOptions = { - environment: options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV, + environment: options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv() || process.env.NODE_ENV, release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, defaultIntegrations: customDefaultIntegrations, // Next.js emits its own OpenTelemetry spans, so it defaults to registering the Sentry tracer diff --git a/packages/nextjs/test/common/getVercelEnv.test.ts b/packages/nextjs/test/common/getVercelEnv.test.ts new file mode 100644 index 000000000000..7bc79c7d71d6 --- /dev/null +++ b/packages/nextjs/test/common/getVercelEnv.test.ts @@ -0,0 +1,30 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getClientVercelEnv } from '../../src/common/getVercelEnv'; + +describe('getClientVercelEnv', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + delete process.env.NEXT_PUBLIC_VERCEL_ENV; + delete process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('returns undefined when no Vercel env vars are set', () => { + expect(getClientVercelEnv()).toBeUndefined(); + }); + + it('prefers NEXT_PUBLIC_VERCEL_TARGET_ENV over NEXT_PUBLIC_VERCEL_ENV', () => { + process.env.NEXT_PUBLIC_VERCEL_ENV = 'preview'; + process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV = 'staging'; + + expect(getClientVercelEnv()).toBe('staging'); + + delete process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV; + expect(getClientVercelEnv()).toBe('preview'); + }); +}); diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 5e6c2926ccbb..70503ad97164 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -10,6 +10,7 @@ import { functionToStringIntegration, getCurrentScope, getIntegrationsToSetup, + getVercelEnv, GLOBAL_OBJ, linkedErrorsIntegration, requestDataIntegration, @@ -25,7 +26,6 @@ import { VercelEdgeClient } from './client'; import { winterCGFetchIntegration } from './integrations/wintercg-fetch'; import { makeEdgeTransport } from './transports'; import type { VercelEdgeOptions } from './types'; -import { getVercelEnv } from './utils/vercel'; declare const process: { env: Record; @@ -76,8 +76,7 @@ export function init(options: VercelEdgeOptions = {}): Client { } } - options.environment = - options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV; + options.environment = options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv() || process.env.NODE_ENV; options.traceLifecycle = options.traceLifecycle ?? getTraceLifecycleFromEnv(process.env.SENTRY_TRACE_LIFECYCLE); diff --git a/packages/vercel-edge/src/utils/vercel.ts b/packages/vercel-edge/src/utils/vercel.ts deleted file mode 100644 index cb640aaff1c5..000000000000 --- a/packages/vercel-edge/src/utils/vercel.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare const process: { - env: Record; -}; - -/** - * Returns an environment setting value determined by Vercel's `VERCEL_ENV` environment variable. - * - * @param isClient Flag to indicate whether to use the `NEXT_PUBLIC_` prefixed version of the environment variable. - */ -export function getVercelEnv(isClient: boolean): string | undefined { - const vercelEnvVar = isClient ? process.env.NEXT_PUBLIC_VERCEL_ENV : process.env.VERCEL_ENV; - return vercelEnvVar ? `vercel-${vercelEnvVar}` : undefined; -}