diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts index 794e34973358..0f5ee6100a19 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts @@ -15,7 +15,7 @@ test('Should create a transaction for edge routes', async ({ request }) => { expect(edgerouteTransaction.contexts?.trace?.status).toBe('ok'); expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); - expect(edgerouteTransaction.contexts?.runtime?.name).toBe('edge'); + expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge'); }); test('Should create a transaction with error status for faulty edge routes', async ({ request }) => { @@ -34,7 +34,7 @@ test('Should create a transaction with error status for faulty edge routes', asy expect(edgerouteTransaction.contexts?.trace?.status).toBe('internal_error'); expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); - expect(edgerouteTransaction.contexts?.runtime?.name).toBe('edge'); + expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge'); }); test('Should record exceptions for faulty edge routes', async ({ request }) => { diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts index cef1c30f3f4f..7cf989236360 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts @@ -13,7 +13,7 @@ test('Should create a transaction for middleware', async ({ request }) => { expect(middlewareTransaction.contexts?.trace?.status).toBe('ok'); expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); - expect(middlewareTransaction.contexts?.runtime?.name).toBe('edge'); + expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge'); }); test('Should create a transaction with error status for faulty middleware', async ({ request }) => { @@ -31,7 +31,7 @@ test('Should create a transaction with error status for faulty middleware', asyn expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error'); expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); - expect(middlewareTransaction.contexts?.runtime?.name).toBe('edge'); + expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge'); }); test('Records exceptions happening in middleware', async ({ request }) => { diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 566df1abb1d1..cbd4414436f6 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -87,9 +87,9 @@ test.describe('Edge runtime', () => { expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error'); expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); - expect(routehandlerTransaction.contexts?.runtime?.name).toBe('edge'); + expect(routehandlerTransaction.contexts?.runtime?.name).toBe('vercel-edge'); expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-edge-error'); - expect(routehandlerError.contexts?.runtime?.name).toBe('edge'); + expect(routehandlerError.contexts?.runtime?.name).toBe('vercel-edge'); }); }); diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index dd598ee56d99..829a4a6e8e6f 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -31,6 +31,7 @@ "@sentry/react": "7.69.0", "@sentry/types": "7.69.0", "@sentry/utils": "7.69.0", + "@sentry/vercel-edge": "7.69.0", "@sentry/webpack-plugin": "1.20.0", "chalk": "3.0.0", "rollup": "2.78.0", diff --git a/packages/nextjs/src/edge/asyncLocalStorageAsyncContextStrategy.ts b/packages/nextjs/src/edge/asyncLocalStorageAsyncContextStrategy.ts deleted file mode 100644 index 36c6317248b4..000000000000 --- a/packages/nextjs/src/edge/asyncLocalStorageAsyncContextStrategy.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core'; -import { ensureHubOnCarrier, getHubFromCarrier, setAsyncContextStrategy } from '@sentry/core'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; - -interface AsyncLocalStorage { - getStore(): T | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; -} - -// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -const MaybeGlobalAsyncLocalStorage = (GLOBAL_OBJ as any).AsyncLocalStorage; - -let asyncStorage: AsyncLocalStorage; - -/** - * Sets the async context strategy to use AsyncLocalStorage which should be available in the edge runtime. - */ -export function setAsyncLocalStorageAsyncContextStrategy(): void { - if (!MaybeGlobalAsyncLocalStorage) { - __DEBUG_BUILD__ && - logger.warn( - "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", - ); - return; - } - - if (!asyncStorage) { - asyncStorage = new MaybeGlobalAsyncLocalStorage(); - } - - function getCurrentHub(): Hub | undefined { - return asyncStorage.getStore(); - } - - function createNewHub(parent: Hub | undefined): Hub { - const carrier: Carrier = {}; - ensureHubOnCarrier(carrier, parent); - return getHubFromCarrier(carrier); - } - - function runWithAsyncContext(callback: () => T, options: RunWithAsyncContextOptions): T { - const existingHub = getCurrentHub(); - - if (existingHub && options?.reuseExisting) { - // We're already in an async context, so we don't need to create a new one - // just call the callback with the current hub - return callback(); - } - - const newHub = createNewHub(existingHub); - - return asyncStorage.run(newHub, () => { - return callback(); - }); - } - - setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext }); -} diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 281dd215001f..0f13ff9cfccd 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,73 +1,13 @@ -import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { - getIntegrationsToSetup, - initAndBind, - Integrations as CoreIntegrations, - SDK_VERSION, - ServerRuntimeClient, -} from '@sentry/core'; -import type { Options } from '@sentry/types'; -import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; +import { SDK_VERSION } from '@sentry/core'; +import type { VercelEdgeOptions } from '@sentry/vercel-edge'; +import { init as vercelEdgeInit } from '@sentry/vercel-edge'; -import { getVercelEnv } from '../common/getVercelEnv'; -import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageAsyncContextStrategy'; -import { makeEdgeTransport } from './transport'; - -const nodeStackParser = createStackParser(nodeStackLineParser()); - -export const defaultIntegrations = [new CoreIntegrations.InboundFilters(), new CoreIntegrations.FunctionToString()]; - -export type EdgeOptions = Options; +export type EdgeOptions = VercelEdgeOptions; /** Inits the Sentry NextJS SDK on the Edge Runtime. */ -export function init(options: EdgeOptions = {}): void { - setAsyncLocalStorageAsyncContextStrategy(); - - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = defaultIntegrations; - } - - if (options.dsn === undefined && process.env.SENTRY_DSN) { - options.dsn = process.env.SENTRY_DSN; - } - - if (options.tracesSampleRate === undefined && process.env.SENTRY_TRACES_SAMPLE_RATE) { - const tracesSampleRate = parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE); - if (isFinite(tracesSampleRate)) { - options.tracesSampleRate = tracesSampleRate; - } - } - - if (options.release === undefined) { - const detectedRelease = getSentryRelease(); - if (detectedRelease !== undefined) { - options.release = detectedRelease; - } else { - // If release is not provided, then we should disable autoSessionTracking - options.autoSessionTracking = false; - } - } - - options.environment = - options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV; - - if (options.autoSessionTracking === undefined && options.dsn !== undefined) { - options.autoSessionTracking = true; - } - - if (options.instrumenter === undefined) { - options.instrumenter = 'sentry'; - } - - const clientOptions: ServerRuntimeClientOptions = { - ...options, - stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), - integrations: getIntegrationsToSetup(options), - transport: options.transport || makeEdgeTransport, - }; - - clientOptions._metadata = clientOptions._metadata || {}; - clientOptions._metadata.sdk = clientOptions._metadata.sdk || { +export function init(options: VercelEdgeOptions = {}): void { + options._metadata = options._metadata || {}; + options._metadata.sdk = options._metadata.sdk || { name: 'sentry.javascript.nextjs', packages: [ { @@ -78,45 +18,7 @@ export function init(options: EdgeOptions = {}): void { version: SDK_VERSION, }; - clientOptions.platform = 'edge'; - clientOptions.runtime = { name: 'edge' }; - clientOptions.serverName = process.env.SENTRY_NAME; - - initAndBind(ServerRuntimeClient, clientOptions); - - // TODO?: Sessiontracking -} - -/** - * Returns a release dynamically from environment variables. - */ -export function getSentryRelease(fallback?: string): string | undefined { - // Always read first as Sentry takes this as precedence - if (process.env.SENTRY_RELEASE) { - return process.env.SENTRY_RELEASE; - } - - // This supports the variable that sentry-webpack-plugin injects - if (GLOBAL_OBJ.SENTRY_RELEASE && GLOBAL_OBJ.SENTRY_RELEASE.id) { - return GLOBAL_OBJ.SENTRY_RELEASE.id; - } - - return ( - // GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables - process.env.GITHUB_SHA || - // Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata - process.env.COMMIT_REF || - // Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables - process.env.VERCEL_GIT_COMMIT_SHA || - process.env.VERCEL_GITHUB_COMMIT_SHA || - process.env.VERCEL_GITLAB_COMMIT_SHA || - process.env.VERCEL_BITBUCKET_COMMIT_SHA || - // Zeit (now known as Vercel) - process.env.ZEIT_GITHUB_COMMIT_SHA || - process.env.ZEIT_GITLAB_COMMIT_SHA || - process.env.ZEIT_BITBUCKET_COMMIT_SHA || - fallback - ); + vercelEdgeInit(options); } /** @@ -126,7 +28,8 @@ export function withSentryConfig(exportedUserNextConfig: T): T { return exportedUserNextConfig; } -export * from '@sentry/core'; +export * from '@sentry/vercel-edge'; +export { Span, Transaction } from '@sentry/core'; // eslint-disable-next-line import/export export * from '../common'; diff --git a/packages/nextjs/src/edge/transport.ts b/packages/nextjs/src/edge/transport.ts deleted file mode 100644 index 9c41ddf149c1..000000000000 --- a/packages/nextjs/src/edge/transport.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { createTransport } from '@sentry/core'; -import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; -import { SentryError } from '@sentry/utils'; - -export interface EdgeTransportOptions extends BaseTransportOptions { - /** Fetch API init parameters. Used by the FetchTransport */ - fetchOptions?: RequestInit; - /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ - headers?: { [key: string]: string }; -} - -const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; - -/** - * This is a modified promise buffer that collects tasks until drain is called. - * We need this in the edge runtime because edge function invocations may not share I/O objects, like fetch requests - * and responses, and the normal PromiseBuffer inherently buffers stuff inbetween incoming requests. - * - * A limitation we need to be aware of is that DEFAULT_TRANSPORT_BUFFER_SIZE is the maximum amount of payloads the - * SDK can send for a given edge function invocation. - */ -export class IsolatedPromiseBuffer { - // We just have this field because the promise buffer interface requires it. - // If we ever remove it from the interface we should also remove it here. - public $: Array>; - - private _taskProducers: (() => PromiseLike)[]; - - private readonly _bufferSize: number; - - public constructor(_bufferSize = DEFAULT_TRANSPORT_BUFFER_SIZE) { - this.$ = []; - this._taskProducers = []; - this._bufferSize = _bufferSize; - } - - /** - * @inheritdoc - */ - public add(taskProducer: () => PromiseLike): PromiseLike { - if (this._taskProducers.length >= this._bufferSize) { - return Promise.reject(new SentryError('Not adding Promise because buffer limit was reached.')); - } - - this._taskProducers.push(taskProducer); - return Promise.resolve(); - } - - /** - * @inheritdoc - */ - public drain(timeout?: number): PromiseLike { - const oldTaskProducers = [...this._taskProducers]; - this._taskProducers = []; - - return new Promise(resolve => { - const timer = setTimeout(() => { - if (timeout && timeout > 0) { - resolve(false); - } - }, timeout); - - void Promise.all( - oldTaskProducers.map(taskProducer => - taskProducer().then(null, () => { - // catch all failed requests - }), - ), - ).then(() => { - // resolve to true if all fetch requests settled - clearTimeout(timer); - resolve(true); - }); - }); - } -} - -/** - * Creates a Transport that uses the Edge Runtimes native fetch API to send events to Sentry. - */ -export function makeEdgeTransport(options: EdgeTransportOptions): Transport { - function makeRequest(request: TransportRequest): PromiseLike { - const requestOptions: RequestInit = { - body: request.body, - method: 'POST', - referrerPolicy: 'origin', - headers: options.headers, - ...options.fetchOptions, - }; - - return fetch(options.url, requestOptions).then(response => { - return { - statusCode: response.status, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - }; - }); - } - - return createTransport(options, makeRequest, new IsolatedPromiseBuffer(options.bufferSize)); -} diff --git a/packages/nextjs/test/edge/transport.test.ts b/packages/nextjs/test/edge/transport.test.ts deleted file mode 100644 index 26be44c56e95..000000000000 --- a/packages/nextjs/test/edge/transport.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { EventEnvelope, EventItem } from '@sentry/types'; -import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import { TextEncoder } from 'util'; - -import type { EdgeTransportOptions } from '../../src/edge/transport'; -import { IsolatedPromiseBuffer, makeEdgeTransport } from '../../src/edge/transport'; - -const DEFAULT_EDGE_TRANSPORT_OPTIONS: EdgeTransportOptions = { - url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', - recordDroppedEvent: () => undefined, - textEncoder: new TextEncoder(), -}; - -const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ - [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, -]); - -class Headers { - headers: { [key: string]: string } = {}; - get(key: string) { - return this.headers[key] || null; - } - set(key: string, value: string) { - this.headers[key] = value; - } -} - -const mockFetch = jest.fn(); - -// @ts-expect-error fetch is not on global -const oldFetch = global.fetch; -// @ts-expect-error fetch is not on global -global.fetch = mockFetch; - -afterAll(() => { - // @ts-expect-error fetch is not on global - global.fetch = oldFetch; -}); - -describe('Edge Transport', () => { - it('calls fetch with the given URL', async () => { - mockFetch.mockImplementationOnce(() => - Promise.resolve({ - headers: new Headers(), - status: 200, - text: () => Promise.resolve({}), - }), - ); - - const transport = makeEdgeTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS); - - expect(mockFetch).toHaveBeenCalledTimes(0); - await transport.send(ERROR_ENVELOPE); - await transport.flush(); - expect(mockFetch).toHaveBeenCalledTimes(1); - - expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_EDGE_TRANSPORT_OPTIONS.url, { - body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), - method: 'POST', - referrerPolicy: 'origin', - }); - }); - - it('sets rate limit headers', async () => { - const headers = { - get: jest.fn(), - }; - - mockFetch.mockImplementationOnce(() => - Promise.resolve({ - headers, - status: 200, - text: () => Promise.resolve({}), - }), - ); - - const transport = makeEdgeTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS); - - expect(headers.get).toHaveBeenCalledTimes(0); - await transport.send(ERROR_ENVELOPE); - await transport.flush(); - - expect(headers.get).toHaveBeenCalledTimes(2); - expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); - expect(headers.get).toHaveBeenCalledWith('Retry-After'); - }); - - it('allows for custom options to be passed in', async () => { - mockFetch.mockImplementationOnce(() => - Promise.resolve({ - headers: new Headers(), - status: 200, - text: () => Promise.resolve({}), - }), - ); - - const REQUEST_OPTIONS: RequestInit = { - referrerPolicy: 'strict-origin', - keepalive: false, - referrer: 'http://example.org', - }; - - const transport = makeEdgeTransport({ ...DEFAULT_EDGE_TRANSPORT_OPTIONS, fetchOptions: REQUEST_OPTIONS }); - - await transport.send(ERROR_ENVELOPE); - await transport.flush(); - expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_EDGE_TRANSPORT_OPTIONS.url, { - body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), - method: 'POST', - ...REQUEST_OPTIONS, - }); - }); -}); - -describe('IsolatedPromiseBuffer', () => { - it('should not call tasks until drained', async () => { - const ipb = new IsolatedPromiseBuffer(); - - const task1 = jest.fn(() => Promise.resolve({})); - const task2 = jest.fn(() => Promise.resolve({})); - - await ipb.add(task1); - await ipb.add(task2); - - expect(task1).not.toHaveBeenCalled(); - expect(task2).not.toHaveBeenCalled(); - - await ipb.drain(); - - expect(task1).toHaveBeenCalled(); - expect(task2).toHaveBeenCalled(); - }); - - it('should not allow adding more items than the specified limit', async () => { - const ipb = new IsolatedPromiseBuffer(3); - - const task1 = jest.fn(() => Promise.resolve({})); - const task2 = jest.fn(() => Promise.resolve({})); - const task3 = jest.fn(() => Promise.resolve({})); - const task4 = jest.fn(() => Promise.resolve({})); - - await ipb.add(task1); - await ipb.add(task2); - await ipb.add(task3); - - await expect(ipb.add(task4)).rejects.toThrowError('Not adding Promise because buffer limit was reached.'); - }); - - it('should not throw when one of the tasks throws when drained', async () => { - const ipb = new IsolatedPromiseBuffer(); - - const task1 = jest.fn(() => Promise.resolve({})); - const task2 = jest.fn(() => Promise.reject(new Error())); - - await ipb.add(task1); - await ipb.add(task2); - - await expect(ipb.drain()).resolves.toEqual(true); - - expect(task1).toHaveBeenCalled(); - expect(task2).toHaveBeenCalled(); - }); -}); diff --git a/packages/nextjs/test/integration/package.json b/packages/nextjs/test/integration/package.json index a382ac88f62c..b62f4583e188 100644 --- a/packages/nextjs/test/integration/package.json +++ b/packages/nextjs/test/integration/package.json @@ -34,6 +34,7 @@ "@sentry/tracing": "file:../../../tracing", "@sentry-internal/tracing": "file:../../../tracing-internal", "@sentry/types": "file:../../../types", - "@sentry/utils": "file:../../../utils" + "@sentry/utils": "file:../../../utils", + "@sentry/vercel-edge": "file:../../../vercel-edge" } }