diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts index 29e2ee55e45e..e8cde6e94baf 100644 --- a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -1,12 +1,6 @@ import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; -import { - getClient, - GLOBAL_OBJ, - isSentryRequestUrl, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - type Span, - type SpanAttributes, -} from '@sentry/core'; +import { GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span, type SpanAttributes } from '@sentry/core'; +import { isSentryRequestSpan } from '@sentry/opentelemetry'; import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; @@ -42,36 +36,6 @@ export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes | } } -/** - * Local copy of `@sentry/opentelemetry`'s `isSentryRequestSpan`, to avoid pulling the whole package into Edge bundles. - */ -function isSentryRequestSpan(span: Span): boolean { - const attributes = spanToAttributes(span); - if (!attributes) { - return false; - } - - const httpUrl = attributes['http.url'] || attributes['url.full']; - if (!httpUrl) { - return false; - } - - return isSentryRequestUrl(httpUrl.toString(), getClient()); -} - -function spanToAttributes(span: Span): Record | undefined { - // OTEL spans expose attributes in different shapes depending on implementation. - // We only need best-effort read access. - type MaybeSpanAttributes = { - attributes?: Record; - _attributes?: Record; - }; - - const maybeSpan = span as unknown as MaybeSpanAttributes; - const attrs = maybeSpan.attributes || maybeSpan._attributes; - return attrs; -} - /** * Checks if a span's HTTP target matches the tunnel route. */ diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 94c71a52c483..9fa05c94e978 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,7 +1,7 @@ // import/export got a false positive, and affects most of our index barrel files // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ -import { context, createContextKey } from '@opentelemetry/api'; +import { context } from '@opentelemetry/api'; import { applySdkMetadata, type EventProcessor, @@ -12,7 +12,6 @@ import { getRootSpan, GLOBAL_OBJ, registerSpanErrorInstrumentation, - type Scope, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -20,6 +19,7 @@ import { spanToJSON, stripUrlQueryAndFragment, } from '@sentry/core'; +import { getScopesFromContext } from '@sentry/opentelemetry'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -42,32 +42,6 @@ export { wrapApiHandlerWithSentry } from './wrapApiHandlerWithSentry'; export type EdgeOptions = VercelEdgeOptions; -type CurrentScopes = { - scope: Scope; - isolationScope: Scope; -}; - -// This key must match `@sentry/opentelemetry`'s `SENTRY_SCOPES_CONTEXT_KEY`. -// We duplicate it here so the Edge bundle does not need to import the full `@sentry/opentelemetry` package. -const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); - -type ContextWithGetValue = { - getValue(key: unknown): unknown; -}; - -function getScopesFromContext(otelContext: unknown): CurrentScopes | undefined { - if (!otelContext || typeof otelContext !== 'object') { - return undefined; - } - - const maybeContext = otelContext as Partial; - if (typeof maybeContext.getValue !== 'function') { - return undefined; - } - - return maybeContext.getValue(SENTRY_SCOPES_CONTEXT_KEY) as CurrentScopes | undefined; -} - const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir?: string; _sentryRelease?: string; diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts index 3500ad6c4782..375e42dfdd00 100644 --- a/packages/opentelemetry/src/constants.ts +++ b/packages/opentelemetry/src/constants.ts @@ -9,10 +9,6 @@ export const SENTRY_TRACE_STATE_URL = 'sentry.url'; export const SENTRY_TRACE_STATE_SAMPLE_RAND = 'sentry.sample_rand'; export const SENTRY_TRACE_STATE_SAMPLE_RATE = 'sentry.sample_rate'; -// NOTE: `@sentry/nextjs` has a local copy of this context key for Edge bundles: -// - `packages/nextjs/src/edge/index.ts` (`SENTRY_SCOPES_CONTEXT_KEY`) -// -// If you change the key name passed to `createContextKey(...)`, update that file too. export const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); export const SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY = createContextKey('sentry_fork_isolation_scope'); diff --git a/packages/opentelemetry/src/utils/contextData.ts b/packages/opentelemetry/src/utils/contextData.ts index 78577131d0c7..468b377f9ccd 100644 --- a/packages/opentelemetry/src/utils/contextData.ts +++ b/packages/opentelemetry/src/utils/contextData.ts @@ -11,10 +11,6 @@ const SCOPE_CONTEXT_FIELD = '_scopeContext'; * This requires a Context Manager that was wrapped with getWrappedContextManager. */ export function getScopesFromContext(context: Context): CurrentScopes | undefined { - // NOTE: `@sentry/nextjs` has a local copy of this helper for Edge bundles: - // - `packages/nextjs/src/edge/index.ts` (`getScopesFromContext`) - // - // If you change how scopes are stored/read (key or retrieval), update that file too. return context.getValue(SENTRY_SCOPES_CONTEXT_KEY) as CurrentScopes | undefined; } diff --git a/packages/opentelemetry/src/utils/isSentryRequest.ts b/packages/opentelemetry/src/utils/isSentryRequest.ts index 6e06bcf5ab2e..d6b59880137b 100644 --- a/packages/opentelemetry/src/utils/isSentryRequest.ts +++ b/packages/opentelemetry/src/utils/isSentryRequest.ts @@ -9,10 +9,6 @@ import { spanHasAttributes } from './spanTypes'; * @returns boolean */ export function isSentryRequestSpan(span: AbstractSpan): boolean { - // NOTE: `@sentry/nextjs` has a local copy of this helper for Edge bundles: - // - `packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts` (`isSentryRequestSpan`) - // - // If you change supported OTEL attribute keys or request detection logic, update that file too. if (!spanHasAttributes(span)) { return false; } diff --git a/packages/vercel-edge/rollup.npm.config.mjs b/packages/vercel-edge/rollup.npm.config.mjs index d8f1704e2f8a..ae01f43703d0 100644 --- a/packages/vercel-edge/rollup.npm.config.mjs +++ b/packages/vercel-edge/rollup.npm.config.mjs @@ -1,138 +1,79 @@ import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants, plugins } from '@sentry-internal/rollup-utils'; -const downlevelLogicalAssignmentsPlugin = { - name: 'downlevel-logical-assignments', - renderChunk(code) { - // ES2021 logical assignment operators (`||=`, `&&=`, `??=`) are not allowed by our ES2020 compatibility check. - // OTEL currently ships some of these, so we downlevel them in the final output. - // - // Note: This is intentionally conservative (only matches property access-like LHS) to avoid duplicating side effects. - // IMPORTANT: Use regex literals (not `String.raw` + `RegExp(...)`) to avoid accidental double-escaping. - let out = code; - - // ??= - out = out.replace(/([A-Za-z_$][\w$]*(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)+)\s*\?\?=\s*([^;]+);/g, (_m, left, right) => { - return `${left} = ${left} ?? ${right};`; - }); - - // ||= - out = out.replace(/([A-Za-z_$][\w$]*(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)+)\s*\|\|=\s*([^;]+);/g, (_m, left, right) => { - return `${left} = ${left} || ${right};`; - }); - - // &&= - out = out.replace(/([A-Za-z_$][\w$]*(?:\[[^\]]+\]|\.[A-Za-z_$][\w$]*)+)\s*&&=\s*([^;]+);/g, (_m, left, right) => { - return `${left} = ${left} && ${right};`; - }); - - return { code: out, map: null }; - }, -}; - -const baseConfig = makeBaseNPMConfig({ - entrypoints: ['src/index.ts'], - bundledBuiltins: ['perf_hooks', 'util'], - packageSpecificConfig: { - context: 'globalThis', - output: { - preserveModules: false, - }, - plugins: [ - plugins.makeCommonJSPlugin({ transformMixedEsModules: true }), // Needed because various modules in the OTEL toolchain use CJS (require-in-the-middle, shimmer, etc..) - plugins.makeJsonPlugin(), // Needed because `require-in-the-middle` imports json via require - replace({ - preventAssignment: true, - values: { - 'process.argv0': JSON.stringify(''), // needed because otel relies on process.argv0 for the default service name, but that api is not available in the edge runtime. - }, - }), - { - // This plugin is needed because otel imports `performance` from `perf_hooks` and also uses it via the `performance` global. - // It also imports `inspect` and `promisify` from node's `util` which are not available in the edge runtime so we need to define a polyfill. - // Both of these APIs are not available in the edge runtime so we need to define a polyfill. - // Vercel does something similar in the `@vercel/otel` package: https://github.com/vercel/otel/blob/087601ae585cb116bb2b46c211d014520de76c71/packages/otel/build.ts#L62 - name: 'edge-runtime-polyfills', - banner: ` - { - if (globalThis.performance === undefined) { - globalThis.performance = { - timeOrigin: 0, - now: () => Date.now() - }; - } - } - `, - resolveId: source => { - if (source === 'perf_hooks') { - return '\0perf_hooks_sentry_shim'; - } else if (source === 'util') { - return '\0util_sentry_shim'; - } else { - return null; - } - }, - load: id => { - if (id === '\0perf_hooks_sentry_shim') { - return ` - export const performance = { - timeOrigin: 0, - now: () => Date.now() +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.ts'], + bundledBuiltins: ['perf_hooks', 'util'], + packageSpecificConfig: { + context: 'globalThis', + output: { + preserveModules: false, + }, + plugins: [ + plugins.makeCommonJSPlugin({ transformMixedEsModules: true }), // Needed because various modules in the OTEL toolchain use CJS (require-in-the-middle, shimmer, etc..) + plugins.makeJsonPlugin(), // Needed because `require-in-the-middle` imports json via require + replace({ + preventAssignment: true, + values: { + 'process.argv0': JSON.stringify(''), // needed because otel relies on process.argv0 for the default service name, but that api is not available in the edge runtime. + }, + }), + { + // This plugin is needed because otel imports `performance` from `perf_hooks` and also uses it via the `performance` global. + // It also imports `inspect` and `promisify` from node's `util` which are not available in the edge runtime so we need to define a polyfill. + // Both of these APIs are not available in the edge runtime so we need to define a polyfill. + // Vercel does something similar in the `@vercel/otel` package: https://github.com/vercel/otel/blob/087601ae585cb116bb2b46c211d014520de76c71/packages/otel/build.ts#L62 + name: 'edge-runtime-polyfills', + banner: ` + { + if (globalThis.performance === undefined) { + globalThis.performance = { + timeOrigin: 0, + now: () => Date.now() + }; } - `; - } else if (id === '\0util_sentry_shim') { - return ` - export const inspect = (object) => - JSON.stringify(object, null, 2); + } + `, + resolveId: source => { + if (source === 'perf_hooks') { + return '\0perf_hooks_sentry_shim'; + } else if (source === 'util') { + return '\0util_sentry_shim'; + } else { + return null; + } + }, + load: id => { + if (id === '\0perf_hooks_sentry_shim') { + return ` + export const performance = { + timeOrigin: 0, + now: () => Date.now() + } + `; + } else if (id === '\0util_sentry_shim') { + return ` + export const inspect = (object) => + JSON.stringify(object, null, 2); - export const promisify = (fn) => { - return (...args) => { - return new Promise((resolve, reject) => { - fn(...args, (err, result) => { - if (err) reject(err); - else resolve(result); + export const promisify = (fn) => { + return (...args) => { + return new Promise((resolve, reject) => { + fn(...args, (err, result) => { + if (err) reject(err); + else resolve(result); + }); }); - }); + }; }; - }; - `; - } else { - return null; - } + `; + } else { + return null; + } + }, }, - }, - downlevelLogicalAssignmentsPlugin, - ], - }, -}); - -// `makeBaseNPMConfig` marks dependencies/peers as external by default. -// For Edge, we must ensure the OTEL SDK bits which reference `process.argv0` are bundled so our replace() plugin applies. -const baseExternal = baseConfig.external; -baseConfig.external = (source, importer, isResolved) => { - // Never treat these as external - they need to be inlined so `process.argv0` can be replaced. - if ( - source === '@opentelemetry/resources' || - source.startsWith('@opentelemetry/resources/') || - source === '@opentelemetry/sdk-trace-base' || - source.startsWith('@opentelemetry/sdk-trace-base/') - ) { - return false; - } - - if (typeof baseExternal === 'function') { - return baseExternal(source, importer, isResolved); - } - - if (Array.isArray(baseExternal)) { - return baseExternal.includes(source); - } - - if (baseExternal instanceof RegExp) { - return baseExternal.test(source); - } - - return false; -}; - -export default makeNPMConfigVariants(baseConfig); + ], + }, + }), +); diff --git a/packages/vercel-edge/test/build-artifacts.test.ts b/packages/vercel-edge/test/build-artifacts.test.ts deleted file mode 100644 index c4994f4f8b29..000000000000 --- a/packages/vercel-edge/test/build-artifacts.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { describe, expect, it } from 'vitest'; - -function readBuildFile(relativePathFromPackageRoot: string): string { - const filePath = join(process.cwd(), relativePathFromPackageRoot); - return readFileSync(filePath, 'utf8'); -} - -describe('build artifacts', () => { - it('does not contain Node-only `process.argv0` usage (Edge compatibility)', () => { - const cjs = readBuildFile('build/cjs/index.js'); - const esm = readBuildFile('build/esm/index.js'); - - expect(cjs).not.toContain('process.argv0'); - expect(esm).not.toContain('process.argv0'); - }); - - it('does not contain ES2021 logical assignment operators (ES2020 compatibility)', () => { - const cjs = readBuildFile('build/cjs/index.js'); - const esm = readBuildFile('build/esm/index.js'); - - // ES2021 operators which `es-check es2020` rejects - expect(cjs).not.toContain('??='); - expect(cjs).not.toContain('||='); - expect(cjs).not.toContain('&&='); - - expect(esm).not.toContain('??='); - expect(esm).not.toContain('||='); - expect(esm).not.toContain('&&='); - }); -});