diff --git a/package.json b/package.json
index 849d79b3628a..04d6c0256860 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"packages/types",
"packages/typescript",
"packages/utils",
+ "packages/vercel-edge",
"packages/vue",
"packages/wasm"
],
diff --git a/packages/vercel-edge/.eslintrc.js b/packages/vercel-edge/.eslintrc.js
new file mode 100644
index 000000000000..99fcba0976da
--- /dev/null
+++ b/packages/vercel-edge/.eslintrc.js
@@ -0,0 +1,23 @@
+module.exports = {
+ env: {
+ node: true,
+ },
+ extends: ['../../.eslintrc.js'],
+ rules: {
+ '@sentry-internal/sdk/no-optional-chaining': 'off',
+ },
+ overrides: [
+ {
+ files: ['scripts/**/*.ts'],
+ parserOptions: {
+ project: ['../../tsconfig.dev.json'],
+ },
+ },
+ {
+ files: ['test/**'],
+ parserOptions: {
+ sourceType: 'module',
+ },
+ },
+ ],
+};
diff --git a/packages/vercel-edge/LICENSE b/packages/vercel-edge/LICENSE
new file mode 100644
index 000000000000..5113ccb2ac3d
--- /dev/null
+++ b/packages/vercel-edge/LICENSE
@@ -0,0 +1,14 @@
+Copyright (c) 2020 Sentry (https://sentry.io) and individual contributors. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/vercel-edge/README.md b/packages/vercel-edge/README.md
new file mode 100644
index 000000000000..aec2e5e27872
--- /dev/null
+++ b/packages/vercel-edge/README.md
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+# Official Sentry SDK for Serverless environments
+
+## Links
+
+- [Official SDK Docs](https://docs.sentry.io/)
+- [TypeDoc](http://getsentry.github.io/sentry-javascript/)
+
+## General
+
+This package is a wrapper around `@sentry/node`, with added functionality related to various Serverless solutions. All
+methods available in `@sentry/node` can be imported from `@sentry/serverless`.
+
+Currently supported environment:
+
+### AWS Lambda
+
+To use this SDK, call `Sentry.AWSLambda.init(options)` at the very beginning of your JavaScript file.
+
+```javascript
+import * as Sentry from '@sentry/serverless';
+
+Sentry.AWSLambda.init({
+ dsn: '__DSN__',
+ // ...
+});
+
+// async (recommended)
+exports.handler = Sentry.AWSLambda.wrapHandler(async (event, context) => {
+ throw new Error('oh, hello there!');
+});
+
+// sync
+exports.handler = Sentry.AWSLambda.wrapHandler((event, context, callback) => {
+ throw new Error('oh, hello there!');
+});
+```
+
+If you also want to trace performance of all the incoming requests and also outgoing AWS service requests, just set the `tracesSampleRate` option.
+
+```javascript
+import * as Sentry from '@sentry/serverless';
+
+Sentry.AWSLambda.init({
+ dsn: '__DSN__',
+ tracesSampleRate: 1.0,
+});
+```
+
+#### Integrate Sentry using internal extension
+
+Another and much simpler way to integrate Sentry to your AWS Lambda function is to add an official layer.
+
+1. Choose Layers -> Add Layer.
+2. Specify an ARN: `arn:aws:lambda:us-west-1:TODO:layer:TODO:VERSION`.
+3. Go to Environment variables and add:
+ - `NODE_OPTIONS`: `-r @sentry/serverless/build/npm/cjs/awslambda-auto`.
+ - `SENTRY_DSN`: `your dsn`.
+ - `SENTRY_TRACES_SAMPLE_RATE`: a number between 0 and 1 representing the chance a transaction is sent to Sentry. For more information, see [docs](https://docs.sentry.io/platforms/node/guides/aws-lambda/configuration/options/#tracesSampleRate).
+
+### Google Cloud Functions
+
+To use this SDK, call `Sentry.GCPFunction.init(options)` at the very beginning of your JavaScript file.
+
+```javascript
+import * as Sentry from '@sentry/serverless';
+
+Sentry.GCPFunction.init({
+ dsn: '__DSN__',
+ tracesSampleRate: 1.0,
+ // ...
+});
+
+// For HTTP Functions:
+
+exports.helloHttp = Sentry.GCPFunction.wrapHttpFunction((req, res) => {
+ throw new Error('oh, hello there!');
+});
+
+// For Background Functions:
+
+exports.helloEvents = Sentry.GCPFunction.wrapEventFunction((data, context, callback) => {
+ throw new Error('oh, hello there!');
+});
+
+// For CloudEvents:
+
+exports.helloEvents = Sentry.GCPFunction.wrapCloudEventFunction((context, callback) => {
+ throw new Error('oh, hello there!');
+});
+```
diff --git a/packages/vercel-edge/jest.config.js b/packages/vercel-edge/jest.config.js
new file mode 100644
index 000000000000..24f49ab59a4c
--- /dev/null
+++ b/packages/vercel-edge/jest.config.js
@@ -0,0 +1 @@
+module.exports = require('../../jest/jest.config.js');
diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json
new file mode 100644
index 000000000000..94b7b213f9d2
--- /dev/null
+++ b/packages/vercel-edge/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@sentry/vercel-edge",
+ "version": "7.61.0",
+ "description": "Official Sentry SDK for the Vercel edge runtime",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge",
+ "author": "Sentry",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "main": "build/npm/cjs/index.js",
+ "module": "build/npm/esm/index.js",
+ "types": "build/npm/types/index.d.ts",
+ "typesVersions": {
+ "<4.9": {
+ "build/npm/types/index.d.ts": [
+ "build/npm/types-ts3.8/index.d.ts"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@sentry/core": "7.61.0",
+ "@sentry/node": "7.61.0",
+ "@sentry/types": "7.61.0",
+ "@sentry/utils": "7.61.0",
+ "@types/express": "^4.17.14",
+ "tslib": "^2.4.1 || ^1.9.3"
+ },
+ "devDependencies": {
+ "@types/node": "^14.6.4",
+ "find-up": "^5.0.0",
+ "nock": "^13.0.4",
+ "npm-packlist": "^2.1.4"
+ },
+ "scripts": {
+ "build": "run-p build:transpile build:types build:bundle",
+ "build:dev": "run-p build:transpile build:types",
+ "build:transpile": "rollup -c rollup.npm.config.js",
+ "build:types": "run-s build:types:core build:types:downlevel",
+ "build:types:core": "tsc -p tsconfig.types.json",
+ "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8",
+ "build:watch": "run-p build:transpile:watch build:types:watch",
+ "build:dev:watch": "yarn build:watch",
+ "build:transpile:watch": "rollup -c rollup.npm.config.js --watch",
+ "build:types:watch": "tsc -p tsconfig.types.json --watch",
+ "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm",
+ "circularDepCheck": "madge --circular src/index.ts",
+ "clean": "rimraf build coverage sentry-vercel-edge-*.tgz",
+ "fix": "run-s fix:eslint fix:prettier",
+ "fix:eslint": "eslint . --format stylish --fix",
+ "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"",
+ "lint": "run-s lint:prettier lint:eslint",
+ "lint:eslint": "eslint . --format stylish",
+ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false,
+ "nx": {
+ "targets": {
+ "build:bundle": {
+ "dependsOn": [
+ "build:transpile",
+ "build:types"
+ ],
+ "outputs": [
+ "{projectRoot}/build/aws"
+ ]
+ }
+ }
+ }
+}
diff --git a/packages/vercel-edge/rollup.npm.config.js b/packages/vercel-edge/rollup.npm.config.js
new file mode 100644
index 000000000000..11f2b66e96ca
--- /dev/null
+++ b/packages/vercel-edge/rollup.npm.config.js
@@ -0,0 +1,9 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js';
+
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ entrypoints: ['src/index.ts'],
+ // packages with bundles have a different build directory structure
+ hasBundles: true,
+ }),
+);
diff --git a/packages/vercel-edge/src/edgeclient.ts b/packages/vercel-edge/src/edgeclient.ts
new file mode 100644
index 000000000000..a5ecca1181b6
--- /dev/null
+++ b/packages/vercel-edge/src/edgeclient.ts
@@ -0,0 +1,175 @@
+import type { Scope } from '@sentry/core';
+import {
+ addTracingExtensions,
+ BaseClient,
+ createCheckInEnvelope,
+ getDynamicSamplingContextFromClient,
+ SDK_VERSION,
+} from '@sentry/core';
+import type {
+ CheckIn,
+ ClientOptions,
+ DynamicSamplingContext,
+ Event,
+ EventHint,
+ MonitorConfig,
+ SerializedCheckIn,
+ Severity,
+ SeverityLevel,
+ TraceContext,
+} from '@sentry/types';
+import { logger, uuid4 } from '@sentry/utils';
+
+import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
+import type { EdgeTransportOptions } from './transport';
+
+export type EdgeClientOptions = ClientOptions;
+
+/**
+ * The Sentry Edge SDK Client.
+ */
+export class EdgeClient extends BaseClient {
+ /**
+ * Creates a new Edge SDK instance.
+ * @param options Configuration options for this SDK.
+ */
+ public constructor(options: EdgeClientOptions) {
+ options._metadata = options._metadata || {};
+ // TODO (isaacharrisholt) figure out how to get a default SDK name
+ options._metadata.sdk = options._metadata.sdk || {
+ name: 'sentry.javascript.nextjs',
+ packages: [
+ {
+ name: 'npm:@sentry/nextjs',
+ version: SDK_VERSION,
+ },
+ ],
+ version: SDK_VERSION,
+ };
+
+ // The Edge client always supports tracing
+ addTracingExtensions();
+
+ super(options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public eventFromException(exception: unknown, hint?: EventHint): PromiseLike {
+ return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public eventFromMessage(
+ message: string,
+ // eslint-disable-next-line deprecation/deprecation
+ level: Severity | SeverityLevel = 'info',
+ hint?: EventHint,
+ ): PromiseLike {
+ return Promise.resolve(
+ eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace),
+ );
+ }
+
+ /**
+ * Create a cron monitor check in and send it to Sentry.
+ *
+ * @param checkIn An object that describes a check in.
+ * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
+ * to create a monitor automatically when sending a check in.
+ */
+ public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string {
+ const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4();
+ if (!this._isEnabled()) {
+ __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.');
+ return id;
+ }
+
+ const options = this.getOptions();
+ const { release, environment, tunnel } = options;
+
+ const serializedCheckIn: SerializedCheckIn = {
+ check_in_id: id,
+ monitor_slug: checkIn.monitorSlug,
+ status: checkIn.status,
+ release,
+ environment,
+ };
+
+ if (checkIn.status !== 'in_progress') {
+ serializedCheckIn.duration = checkIn.duration;
+ }
+
+ if (monitorConfig) {
+ serializedCheckIn.monitor_config = {
+ schedule: monitorConfig.schedule,
+ checkin_margin: monitorConfig.checkinMargin,
+ max_runtime: monitorConfig.maxRuntime,
+ timezone: monitorConfig.timezone,
+ };
+ }
+
+ const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope);
+ if (traceContext) {
+ serializedCheckIn.contexts = {
+ trace: traceContext,
+ };
+ }
+
+ const envelope = createCheckInEnvelope(
+ serializedCheckIn,
+ dynamicSamplingContext,
+ this.getSdkMetadata(),
+ tunnel,
+ this.getDsn(),
+ );
+
+ __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status);
+ void this._sendEnvelope(envelope);
+ return id;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike {
+ event.platform = event.platform || 'edge';
+ event.contexts = {
+ ...event.contexts,
+ runtime: event.contexts?.runtime || {
+ name: 'edge',
+ },
+ };
+ event.server_name = event.server_name || process.env.SENTRY_NAME;
+ return super._prepareEvent(event, hint, scope);
+ }
+
+ /** Extract trace information from scope */
+ private _getTraceInfoFromScope(
+ scope: Scope | undefined,
+ ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] {
+ if (!scope) {
+ return [undefined, undefined];
+ }
+
+ const span = scope.getSpan();
+ if (span) {
+ return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()];
+ }
+
+ const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext();
+ const traceContext: TraceContext = {
+ trace_id: traceId,
+ span_id: spanId,
+ parent_span_id: parentSpanId,
+ };
+ if (dsc) {
+ return [dsc, traceContext];
+ }
+
+ return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext];
+ }
+}
diff --git a/packages/vercel-edge/src/eventbuilder.ts b/packages/vercel-edge/src/eventbuilder.ts
new file mode 100644
index 000000000000..4e483fce3ff7
--- /dev/null
+++ b/packages/vercel-edge/src/eventbuilder.ts
@@ -0,0 +1,130 @@
+import { getCurrentHub } from '@sentry/core';
+import type {
+ Event,
+ EventHint,
+ Exception,
+ Mechanism,
+ Severity,
+ SeverityLevel,
+ StackFrame,
+ StackParser,
+} from '@sentry/types';
+import {
+ addExceptionMechanism,
+ addExceptionTypeValue,
+ extractExceptionKeysForMessage,
+ isError,
+ isPlainObject,
+ normalizeToSize,
+} from '@sentry/utils';
+
+/**
+ * Extracts stack frames from the error.stack string
+ */
+export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] {
+ return stackParser(error.stack || '', 1);
+}
+
+/**
+ * Extracts stack frames from the error and builds a Sentry Exception
+ */
+export function exceptionFromError(stackParser: StackParser, error: Error): Exception {
+ const exception: Exception = {
+ type: error.name || error.constructor.name,
+ value: error.message,
+ };
+
+ const frames = parseStackFrames(stackParser, error);
+ if (frames.length) {
+ exception.stacktrace = { frames };
+ }
+
+ return exception;
+}
+
+/**
+ * Builds and Event from a Exception
+ * @hidden
+ */
+export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event {
+ let ex: unknown = exception;
+ const providedMechanism: Mechanism | undefined =
+ hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism;
+ const mechanism: Mechanism = providedMechanism || {
+ handled: true,
+ type: 'generic',
+ };
+
+ if (!isError(exception)) {
+ if (isPlainObject(exception)) {
+ // This will allow us to group events based on top-level keys
+ // which is much better than creating new group when any key/value change
+ const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`;
+
+ const hub = getCurrentHub();
+ const client = hub.getClient();
+ const normalizeDepth = client && client.getOptions().normalizeDepth;
+ hub.configureScope(scope => {
+ scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth));
+ });
+
+ ex = (hint && hint.syntheticException) || new Error(message);
+ (ex as Error).message = message;
+ } else {
+ // This handles when someone does: `throw "something awesome";`
+ // We use synthesized Error here so we can extract a (rough) stack trace.
+ ex = (hint && hint.syntheticException) || new Error(exception as string);
+ (ex as Error).message = exception as string;
+ }
+ mechanism.synthetic = true;
+ }
+
+ const event = {
+ exception: {
+ values: [exceptionFromError(stackParser, ex as Error)],
+ },
+ };
+
+ addExceptionTypeValue(event, undefined, undefined);
+ addExceptionMechanism(event, mechanism);
+
+ return {
+ ...event,
+ event_id: hint && hint.event_id,
+ };
+}
+
+/**
+ * Builds and Event from a Message
+ * @hidden
+ */
+export function eventFromMessage(
+ stackParser: StackParser,
+ message: string,
+ // eslint-disable-next-line deprecation/deprecation
+ level: Severity | SeverityLevel = 'info',
+ hint?: EventHint,
+ attachStacktrace?: boolean,
+): Event {
+ const event: Event = {
+ event_id: hint && hint.event_id,
+ level,
+ message,
+ };
+
+ if (attachStacktrace && hint && hint.syntheticException) {
+ const frames = parseStackFrames(stackParser, hint.syntheticException);
+ if (frames.length) {
+ event.exception = {
+ values: [
+ {
+ value: message,
+ stacktrace: { frames },
+ },
+ ],
+ };
+ }
+ }
+
+ return event;
+}
diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts
new file mode 100644
index 000000000000..e087c2305097
--- /dev/null
+++ b/packages/vercel-edge/src/index.ts
@@ -0,0 +1,137 @@
+import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
+import type { Options } from '@sentry/types';
+import {
+ createStackParser,
+ GLOBAL_OBJ,
+ logger,
+ nodeStackLineParser,
+ stackParserFromStackParserOptions,
+} from '@sentry/utils';
+
+import { EdgeClient } from './edgeclient';
+import { makeEdgeTransport } from './transport';
+import { getVercelEnv } from './utils/getVercelEnv';
+
+const nodeStackParser = createStackParser(nodeStackLineParser());
+
+export const defaultIntegrations = [new CoreIntegrations.InboundFilters(), new CoreIntegrations.FunctionToString()];
+
+export type EdgeOptions = Options;
+
+/** Inits the Sentry Vercel Edge SDK on the Edge Runtime. */
+export function init(options: EdgeOptions = {}): void {
+ 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() || process.env.NODE_ENV;
+
+ if (options.autoSessionTracking === undefined && options.dsn !== undefined) {
+ options.autoSessionTracking = true;
+ }
+
+ if (options.instrumenter === undefined) {
+ options.instrumenter = 'sentry';
+ }
+
+ const clientOptions = {
+ ...options,
+ stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser),
+ integrations: getIntegrationsToSetup(options),
+ transport: options.transport || makeEdgeTransport,
+ };
+
+ initAndBind(EdgeClient, 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
+ );
+}
+
+/**
+ * Call `close()` on the current client, if there is one. See {@link Client.close}.
+ *
+ * @param timeout Maximum time in ms the client should wait to flush its event queue before shutting down. Omitting this
+ * parameter will cause the client to wait until all events are sent before disabling itself.
+ * @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it
+ * doesn't (or if there's no client defined).
+ */
+export async function close(timeout?: number): Promise {
+ const client = getCurrentHub().getClient();
+ if (client) {
+ return client.close(timeout);
+ }
+ __DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. No client defined.');
+ return Promise.resolve(false);
+}
+
+/**
+ * This is the getter for lastEventId.
+ *
+ * @returns The last event id of a captured event.
+ */
+export function lastEventId(): string | undefined {
+ return getCurrentHub().lastEventId();
+}
+
+/**
+ * Just a passthrough in case this is imported from the client.
+ */
+export function withSentryConfig(exportedUserNextConfig: T): T {
+ return exportedUserNextConfig;
+}
+
+export { flush } from './utils/flush';
+
+export * from '@sentry/core';
diff --git a/packages/vercel-edge/src/transport.ts b/packages/vercel-edge/src/transport.ts
new file mode 100644
index 000000000000..de8c20af20f9
--- /dev/null
+++ b/packages/vercel-edge/src/transport.ts
@@ -0,0 +1,97 @@
+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)[] = [];
+
+ public constructor(private readonly _bufferSize: number = DEFAULT_TRANSPORT_BUFFER_SIZE) {}
+
+ /**
+ * @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/vercel-edge/src/types.ts b/packages/vercel-edge/src/types.ts
new file mode 100644
index 000000000000..9a7bd7553719
--- /dev/null
+++ b/packages/vercel-edge/src/types.ts
@@ -0,0 +1,7 @@
+// We cannot make any assumptions about what users define as their handler except maybe that it is a function
+export interface EdgeRouteHandler {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (...args: any[]): any;
+}
+
+export type EdgeCompatibleSDK = 'nextjs';
diff --git a/packages/vercel-edge/src/utils/flush.ts b/packages/vercel-edge/src/utils/flush.ts
new file mode 100644
index 000000000000..5daa52936391
--- /dev/null
+++ b/packages/vercel-edge/src/utils/flush.ts
@@ -0,0 +1,20 @@
+import { getCurrentHub } from '@sentry/core';
+import type { Client } from '@sentry/types';
+import { logger } from '@sentry/utils';
+
+/**
+ * Call `flush()` on the current client, if there is one. See {@link Client.flush}.
+ *
+ * @param timeout Maximum time in ms the client should wait to flush its event queue. Omitting this parameter will cause
+ * the client to wait until all events are sent before resolving the promise.
+ * @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it
+ * doesn't (or if there's no client defined).
+ */
+export async function flush(timeout?: number): Promise {
+ const client = getCurrentHub().getClient();
+ if (client) {
+ return client.flush(timeout);
+ }
+ __DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.');
+ return Promise.resolve(false);
+}
diff --git a/packages/vercel-edge/src/utils/getVercelEnv.ts b/packages/vercel-edge/src/utils/getVercelEnv.ts
new file mode 100644
index 000000000000..b485d2c51ddb
--- /dev/null
+++ b/packages/vercel-edge/src/utils/getVercelEnv.ts
@@ -0,0 +1,9 @@
+/**
+ * Returns an environment setting value determined by Vercel's `VERCEL_ENV` environment variable.
+ *
+ * @param envVarPrefix Prefix to use for the VERCEL_ENV environment variable (e.g. NEXT_PUBLIC_).
+ */
+export function getVercelEnv(envVarPrefix?: string): string | undefined {
+ const vercelEnvVar = process.env[`${envVarPrefix ? envVarPrefix : ''}VERCEL_ENV`];
+ return vercelEnvVar ? `vercel-${vercelEnvVar}` : undefined;
+}
diff --git a/packages/vercel-edge/test/__mocks__/@sentry/node.ts b/packages/vercel-edge/test/__mocks__/@sentry/node.ts
new file mode 100644
index 000000000000..6da20c091780
--- /dev/null
+++ b/packages/vercel-edge/test/__mocks__/@sentry/node.ts
@@ -0,0 +1,69 @@
+const origSentry = jest.requireActual('@sentry/node');
+export const defaultIntegrations = origSentry.defaultIntegrations; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
+export const Handlers = origSentry.Handlers; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
+export const Integrations = origSentry.Integrations;
+export const addRequestDataToEvent = origSentry.addRequestDataToEvent;
+export const SDK_VERSION = '6.6.6';
+export const Severity = {
+ Warning: 'warning',
+};
+export const fakeHub = {
+ configureScope: jest.fn((fn: (arg: any) => any) => fn(fakeScope)),
+ pushScope: jest.fn(() => fakeScope),
+ popScope: jest.fn(),
+ getScope: jest.fn(() => fakeScope),
+ startTransaction: jest.fn(context => ({ ...fakeTransaction, ...context })),
+};
+export const fakeScope = {
+ addEventProcessor: jest.fn(),
+ setTransactionName: jest.fn(),
+ setTag: jest.fn(),
+ setContext: jest.fn(),
+ setSpan: jest.fn(),
+ getTransaction: jest.fn(() => fakeTransaction),
+ setSDKProcessingMetadata: jest.fn(),
+ setPropagationContext: jest.fn(),
+};
+export const fakeSpan = {
+ finish: jest.fn(),
+};
+export const fakeTransaction = {
+ finish: jest.fn(),
+ setHttpStatus: jest.fn(),
+ startChild: jest.fn(() => fakeSpan),
+};
+export const init = jest.fn();
+export const addGlobalEventProcessor = jest.fn();
+export const getCurrentHub = jest.fn(() => fakeHub);
+export const startTransaction = jest.fn(_ => fakeTransaction);
+export const captureException = jest.fn();
+export const captureMessage = jest.fn();
+export const withScope = jest.fn(cb => cb(fakeScope));
+export const flush = jest.fn(() => Promise.resolve());
+
+export const resetMocks = (): void => {
+ fakeTransaction.setHttpStatus.mockClear();
+ fakeTransaction.finish.mockClear();
+ fakeTransaction.startChild.mockClear();
+ fakeSpan.finish.mockClear();
+ fakeHub.configureScope.mockClear();
+ fakeHub.pushScope.mockClear();
+ fakeHub.popScope.mockClear();
+ fakeHub.getScope.mockClear();
+
+ fakeScope.addEventProcessor.mockClear();
+ fakeScope.setTransactionName.mockClear();
+ fakeScope.setTag.mockClear();
+ fakeScope.setContext.mockClear();
+ fakeScope.setSpan.mockClear();
+ fakeScope.getTransaction.mockClear();
+
+ init.mockClear();
+ addGlobalEventProcessor.mockClear();
+ getCurrentHub.mockClear();
+ startTransaction.mockClear();
+ captureException.mockClear();
+ captureMessage.mockClear();
+ withScope.mockClear();
+ flush.mockClear();
+};
diff --git a/packages/vercel-edge/test/__mocks__/dns.ts b/packages/vercel-edge/test/__mocks__/dns.ts
new file mode 100644
index 000000000000..d03aa8d3f84b
--- /dev/null
+++ b/packages/vercel-edge/test/__mocks__/dns.ts
@@ -0,0 +1,2 @@
+export const lookup = jest.fn();
+export const resolveTxt = jest.fn();
diff --git a/packages/vercel-edge/tsconfig.json b/packages/vercel-edge/tsconfig.json
new file mode 100644
index 000000000000..a2731860dfa0
--- /dev/null
+++ b/packages/vercel-edge/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.json",
+
+ "include": ["src/**/*"],
+
+ "compilerOptions": {
+ // package-specific options
+ "target": "ES2018",
+ "resolveJsonModule": true
+ }
+}
diff --git a/packages/vercel-edge/tsconfig.test.json b/packages/vercel-edge/tsconfig.test.json
new file mode 100644
index 000000000000..87f6afa06b86
--- /dev/null
+++ b/packages/vercel-edge/tsconfig.test.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["test/**/*"],
+
+ "compilerOptions": {
+ // should include all types from `./tsconfig.json` plus types for all test frameworks used
+ "types": ["node", "jest"]
+
+ // other package-specific, test-specific options
+ }
+}
diff --git a/packages/vercel-edge/tsconfig.types.json b/packages/vercel-edge/tsconfig.types.json
new file mode 100644
index 000000000000..374fd9bc9364
--- /dev/null
+++ b/packages/vercel-edge/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/npm/types"
+ }
+}