diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 31431df1c0cc..1c7d639db7ed 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -864,13 +864,16 @@ jobs:
'create-remix-app-v2',
'debug-id-sourcemaps',
'nextjs-app-dir',
+ 'nextjs-14',
'react-create-hash-router',
'react-router-6-use-routes',
'standard-frontend-react',
'standard-frontend-react-tracing-import',
'sveltekit',
+ 'sveltekit-2',
'generic-ts3.8',
'node-experimental-fastify-app',
+ 'node-hapi-app',
]
build-command:
- false
diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index ed05e9bfd1af..9801407515cd 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -73,6 +73,12 @@ jobs:
- test-application: 'nextjs-app-dir'
build-command: 'test:build-latest'
label: 'nextjs-app-dir (latest)'
+ - test-application: 'nextjs-14'
+ build-command: 'test:build-canary'
+ label: 'nextjs-14 (canary)'
+ - test-application: 'nextjs-14'
+ build-command: 'test:build-latest'
+ label: 'nextjs-14 (latest)'
- test-application: 'react-create-hash-router'
build-command: 'test:build-canary'
label: 'react-create-hash-router (canary)'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7284b059cd4d..4345f368f6fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,74 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 7.89.0
+
+### Important Changes
+
+#### Deprecations
+
+- **feat(core): Deprecate `configureScope` (#9887)**
+- **feat(core): Deprecate `pushScope` & `popScope` (#9890)**
+
+This release deprecates `configureScope`, `pushScope`, and `popScope`, which will be removed in the upcoming v8 major release.
+
+#### Hapi Integration
+
+- **feat(node): Add Hapi Integration (#9539)**
+
+This release adds an integration for Hapi. It can be used as follows:
+
+```ts
+const Sentry = require('@sentry/node');
+const Hapi = require('@hapi/hapi');
+
+const init = async () => {
+ const server = Hapi.server({
+ // your server configuration ...
+ });
+
+ Sentry.init({
+ dsn: '__DSN__',
+ tracesSampleRate: 1.0,
+ integrations: [
+ new Sentry.Integrations.Hapi({ server }),
+ ],
+ });
+
+ server.route({
+ // your route configuration ...
+ });
+
+ await server.start();
+};
+```
+
+#### SvelteKit 2.0
+
+- **chore(sveltekit): Add SvelteKit 2.0 to peer dependencies (#9861)**
+
+This release adds support for SvelteKit 2.0 in the `@sentry/sveltekit` package. If you're upgrading from SvelteKit 1.x to 2.x and already use the Sentry SvelteKit SDK, no changes apart from upgrading to this (or a newer) version are necessary.
+
+### Other Changes
+
+- feat(core): Add type & utility for function-based integrations (#9818)
+- feat(core): Update `withScope` to return callback return value (#9866)
+- feat(deno): Support `Deno.CronSchedule` for cron jobs (#9880)
+- feat(nextjs): Auto instrument generation functions (#9781)
+- feat(nextjs): Connect server component transactions if there is no incoming trace (#9845)
+- feat(node-experimental): Update to new Scope APIs (#9799)
+- feat(replay): Add `canvas.type` setting (#9877)
+- fix(nextjs): Export `createReduxEnhancer` (#9854)
+- fix(remix): Do not capture thrown redirect responses. (#9909)
+- fix(sveltekit): Add conditional exports (#9872)
+- fix(sveltekit): Avoid capturing 404 errors on client side (#9902)
+- fix(utils): Do not use `Event` type in worldwide (#9864)
+- fix(utils): Support crypto.getRandomValues in old Chromium versions (#9251)
+- fix(utils): Update `eventFromUnknownInput` to avoid scope pollution & `getCurrentHub` (#9868)
+- ref: Use `addBreadcrumb` directly & allow to pass hint (#9867)
+
+Work in this release contributed by @adam187, and @jghinestrosa. Thank you for your contributions!
+
## 7.88.0
### Important Changes
diff --git a/MIGRATION.md b/MIGRATION.md
index 78f5f16d3002..101f9de4469d 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -8,6 +8,18 @@ npx @sentry/migr8@latest
This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes!
+## Deprecate `pushScope` & `popScope` in favor of `withScope`
+
+Instead of manually pushing/popping a scope, you should use `Sentry.withScope(callback: (scope: Scope))` instead.
+
+## Deprecate `configureScope` in favor of using `getCurrentScope()`
+
+Instead of updating the scope in a callback via `configureScope()`, you should access it via `getCurrentScope()` and configure it directly:
+
+```js
+Sentry.getCurrentScope().setTag('xx', 'yy');
+```
+
## Deprecate `addGlobalEventProcessor` in favor of `addEventProcessor`
Instead of using `addGlobalEventProcessor`, you should use `addEventProcessor` which does not add the event processor globally, but to the current client.
diff --git a/codecov.yml b/codecov.yml
index fcc0885b060b..1013e1b11e24 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -3,6 +3,11 @@ codecov:
notify:
require_ci_to_pass: no
+ai_pr_review:
+ enabled: true
+ method: "label"
+ label_name: "ci-codecov-ai-review"
+
coverage:
precision: 2
round: down
diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts
index 88a2490c1d58..25ddc2fd8dcf 100644
--- a/packages/angular/src/tracing.ts
+++ b/packages/angular/src/tracing.ts
@@ -7,7 +7,7 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { NavigationCancel, NavigationError, Router } from '@angular/router';
import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router';
-import { WINDOW, getCurrentHub } from '@sentry/browser';
+import { WINDOW, getCurrentScope } from '@sentry/browser';
import type { Span, Transaction, TransactionContext } from '@sentry/types';
import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils';
import type { Observable } from 'rxjs';
@@ -50,14 +50,7 @@ export const instrumentAngularRouting = routingInstrumentation;
* Grabs active transaction off scope
*/
export function getActiveTransaction(): Transaction | undefined {
- const currentHub = getCurrentHub();
-
- if (currentHub) {
- const scope = currentHub.getScope();
- return scope.getTransaction();
- }
-
- return undefined;
+ return getCurrentScope().getTransaction();
}
/**
diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts
index e290850241c8..635c8847b9bf 100644
--- a/packages/angular/test/tracing.test.ts
+++ b/packages/angular/test/tracing.test.ts
@@ -21,16 +21,12 @@ jest.mock('@sentry/browser', () => {
const original = jest.requireActual('@sentry/browser');
return {
...original,
- getCurrentHub: () => {
+ getCurrentScope() {
return {
- getScope: () => {
- return {
- getTransaction: () => {
- return transaction;
- },
- };
+ getTransaction: () => {
+ return transaction;
},
- } as unknown as Hub;
+ };
},
};
});
diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts
index aa32e9dcc095..2fd98b8a96cd 100644
--- a/packages/astro/src/client/sdk.ts
+++ b/packages/astro/src/client/sdk.ts
@@ -1,6 +1,6 @@
import type { BrowserOptions } from '@sentry/browser';
import { BrowserTracing, init as initBrowserSdk } from '@sentry/browser';
-import { configureScope, hasTracingEnabled } from '@sentry/core';
+import { getCurrentScope, hasTracingEnabled } from '@sentry/core';
import { addOrUpdateIntegration } from '@sentry/utils';
import { applySdkMetadata } from '../common/metadata';
@@ -20,9 +20,7 @@ export function init(options: BrowserOptions): void {
initBrowserSdk(options);
- configureScope(scope => {
- scope.setTag('runtime', 'browser');
- });
+ getCurrentScope().setTag('runtime', 'browser');
}
function addClientIntegrations(options: BrowserOptions): void {
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index c62590180266..adcf95527364 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -17,6 +17,7 @@ export {
captureMessage,
captureCheckIn,
withMonitor,
+ // eslint-disable-next-line deprecation/deprecation
configureScope,
createTransport,
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts
index 4264be2733f5..7f1f544a19e6 100644
--- a/packages/astro/src/server/meta.ts
+++ b/packages/astro/src/server/meta.ts
@@ -1,5 +1,5 @@
import { getDynamicSamplingContextFromClient } from '@sentry/core';
-import type { Hub, Span } from '@sentry/types';
+import type { Client, Scope, Span } from '@sentry/types';
import {
TRACEPARENT_REGEXP,
dynamicSamplingContextToSentryBaggageHeader,
@@ -22,9 +22,11 @@ import {
*
* @returns an object with the two serialized tags
*/
-export function getTracingMetaTags(span: Span | undefined, hub: Hub): { sentryTrace: string; baggage?: string } {
- const scope = hub.getScope();
- const client = hub.getClient();
+export function getTracingMetaTags(
+ span: Span | undefined,
+ scope: Scope,
+ client: Client | undefined,
+): { sentryTrace: string; baggage?: string } {
const { dsc, sampled, traceId } = scope.getPropagationContext();
const transaction = span?.transaction;
diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts
index 37603a2cdb62..7b4a02cceddf 100644
--- a/packages/astro/src/server/middleware.ts
+++ b/packages/astro/src/server/middleware.ts
@@ -1,12 +1,12 @@
import {
captureException,
- configureScope,
continueTrace,
- getCurrentHub,
+ getClient,
+ getCurrentScope,
runWithAsyncContext,
startSpan,
} from '@sentry/node';
-import type { Hub, Span } from '@sentry/types';
+import type { Client, Scope, Span } from '@sentry/types';
import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils';
import type { APIContext, MiddlewareResponseHandler } from 'astro';
@@ -69,7 +69,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
// if there is an active span, we know that this handle call is nested and hence
// we don't create a new domain for it. If we created one, nested server calls would
// create new transactions instead of adding a child span to the currently active span.
- if (getCurrentHub().getScope().getSpan()) {
+ if (getCurrentScope().getSpan()) {
return instrumentRequest(ctx, next, handlerOptions);
}
return runWithAsyncContext(() => {
@@ -106,9 +106,7 @@ async function instrumentRequest(
}
if (options.trackClientIp) {
- configureScope(scope => {
- scope.setUser({ ip_address: ctx.clientAddress });
- });
+ getCurrentScope().setUser({ ip_address: ctx.clientAddress });
}
try {
@@ -141,8 +139,8 @@ async function instrumentRequest(
span.setHttpStatus(originalResponse.status);
}
- const hub = getCurrentHub();
- const client = hub.getClient();
+ const scope = getCurrentScope();
+ const client = getClient();
const contentType = originalResponse.headers.get('content-type');
const isPageloadRequest = contentType && contentType.startsWith('text/html');
@@ -165,7 +163,7 @@ async function instrumentRequest(
start: async controller => {
for await (const chunk of originalBody) {
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk);
- const modifiedHtml = addMetaTagToHead(html, hub, span);
+ const modifiedHtml = addMetaTagToHead(html, scope, client, span);
controller.enqueue(new TextEncoder().encode(modifiedHtml));
}
controller.close();
@@ -187,12 +185,12 @@ async function instrumentRequest(
* This function optimistically assumes that the HTML coming in chunks will not be split
* within the
tag. If this still happens, we simply won't replace anything.
*/
-function addMetaTagToHead(htmlChunk: string, hub: Hub, span?: Span): string {
+function addMetaTagToHead(htmlChunk: string, scope: Scope, client: Client, span?: Span): string {
if (typeof htmlChunk !== 'string') {
return htmlChunk;
}
- const { sentryTrace, baggage } = getTracingMetaTags(span, hub);
+ const { sentryTrace, baggage } = getTracingMetaTags(span, scope, client);
const content = `\n${sentryTrace}\n${baggage}\n`;
return htmlChunk.replace('', content);
}
diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts
index 8c867ca46fc2..e69d27781ed5 100644
--- a/packages/astro/src/server/sdk.ts
+++ b/packages/astro/src/server/sdk.ts
@@ -1,4 +1,4 @@
-import { configureScope } from '@sentry/core';
+import { getCurrentScope } from '@sentry/core';
import type { NodeOptions } from '@sentry/node';
import { init as initNodeSdk } from '@sentry/node';
@@ -13,7 +13,5 @@ export function init(options: NodeOptions): void {
initNodeSdk(options);
- configureScope(scope => {
- scope.setTag('runtime', 'node');
- });
+ getCurrentScope().setTag('runtime', 'node');
}
diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts
index 6298f5f2a20b..279f36395107 100644
--- a/packages/astro/test/server/meta.test.ts
+++ b/packages/astro/test/server/meta.test.ts
@@ -10,22 +10,20 @@ const mockedSpan = {
environment: 'production',
}),
},
-};
+} as any;
-const mockedHub = {
- getScope: () => ({
- getPropagationContext: () => ({
- traceId: '123',
- }),
+const mockedClient = {} as any;
+
+const mockedScope = {
+ getPropagationContext: () => ({
+ traceId: '123',
}),
- getClient: () => ({}),
-};
+} as any;
describe('getTracingMetaTags', () => {
it('returns the tracing tags from the span, if it is provided', () => {
{
- // @ts-expect-error - only passing a partial span object
- const tags = getTracingMetaTags(mockedSpan, mockedHub);
+ const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient);
expect(tags).toEqual({
sentryTrace: '',
@@ -35,10 +33,9 @@ describe('getTracingMetaTags', () => {
});
it('returns propagationContext DSC data if no span is available', () => {
- const tags = getTracingMetaTags(undefined, {
- ...mockedHub,
- // @ts-expect-error - only passing a partial scope object
- getScope: () => ({
+ const tags = getTracingMetaTags(
+ undefined,
+ {
getPropagationContext: () => ({
traceId: '12345678901234567890123456789012',
sampled: true,
@@ -49,8 +46,9 @@ describe('getTracingMetaTags', () => {
trace_id: '12345678901234567890123456789012',
},
}),
- }),
- });
+ } as any,
+ mockedClient,
+ );
expect(tags).toEqual({
sentryTrace: expect.stringMatching(
@@ -73,7 +71,8 @@ describe('getTracingMetaTags', () => {
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
transaction: undefined,
},
- mockedHub,
+ mockedScope,
+ mockedClient,
);
expect(tags).toEqual({
@@ -93,10 +92,8 @@ describe('getTracingMetaTags', () => {
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
transaction: undefined,
},
- {
- ...mockedHub,
- getClient: () => undefined,
- },
+ mockedScope,
+ undefined,
);
expect(tags).toEqual({
diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts
index dc3b0139b965..5e56c6bd70ed 100644
--- a/packages/astro/test/server/middleware.test.ts
+++ b/packages/astro/test/server/middleware.test.ts
@@ -1,5 +1,6 @@
import * as SentryNode from '@sentry/node';
-import { vi } from 'vitest';
+import type { Client } from '@sentry/types';
+import { SpyInstance, vi } from 'vitest';
import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware';
@@ -14,14 +15,17 @@ describe('sentryMiddleware', () => {
const startSpanSpy = vi.spyOn(SentryNode, 'startSpan');
const getSpanMock = vi.fn(() => {});
- // @ts-expect-error only returning a partial hub here
- vi.spyOn(SentryNode, 'getCurrentHub').mockImplementation(() => {
- return {
- getScope: () => ({
+ const setUserMock = vi.fn();
+
+ beforeEach(() => {
+ vi.spyOn(SentryNode, 'getCurrentScope').mockImplementation(() => {
+ return {
+ setUser: setUserMock,
+ setPropagationContext: vi.fn(),
getSpan: getSpanMock,
- }),
- getClient: () => ({}),
- };
+ } as any;
+ });
+ vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client);
});
const nextResult = Promise.resolve(new Response(null, { status: 200, headers: new Headers() }));
@@ -170,10 +174,6 @@ describe('sentryMiddleware', () => {
});
it('attaches client IP and request headers if options are set', async () => {
- const scope = { setUser: vi.fn(), setPropagationContext: vi.fn() };
- // @ts-expect-error, only passing a partial Scope object
- const configureScopeSpy = vi.spyOn(SentryNode, 'configureScope').mockImplementation(cb => cb(scope));
-
const middleware = handleRequest({ trackClientIp: true, trackHeaders: true });
const ctx = {
request: {
@@ -192,8 +192,7 @@ describe('sentryMiddleware', () => {
// @ts-expect-error, a partial ctx object is fine here
await middleware(ctx, next);
- expect(configureScopeSpy).toHaveBeenCalledTimes(1);
- expect(scope.setUser).toHaveBeenCalledWith({ ip_address: '192.168.0.1' });
+ expect(setUserMock).toHaveBeenCalledWith({ ip_address: '192.168.0.1' });
expect(startSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({
diff --git a/packages/browser-integration-tests/suites/replay/dsc/test.ts b/packages/browser-integration-tests/suites/replay/dsc/test.ts
index ffd2cf1877da..4468a254bde4 100644
--- a/packages/browser-integration-tests/suites/replay/dsc/test.ts
+++ b/packages/browser-integration-tests/suites/replay/dsc/test.ts
@@ -21,6 +21,9 @@ sentryTest(
const transactionReq = waitForTransactionRequest(page);
+ // Wait for this to be available
+ await page.waitForFunction('!!window.Replay');
+
await page.evaluate(() => {
(window as unknown as TestWindow).Replay.start();
});
@@ -28,10 +31,9 @@ sentryTest(
await waitForReplayRunning(page);
await page.evaluate(() => {
- (window as unknown as TestWindow).Sentry.configureScope(scope => {
- scope.setUser({ id: 'user123', segment: 'segmentB' });
- scope.setTransactionName('testTransactionDSC');
- });
+ const scope = (window as unknown as TestWindow).Sentry.getCurrentScope();
+ scope.setUser({ id: 'user123', segment: 'segmentB' });
+ scope.setTransactionName('testTransactionDSC');
});
const req0 = await transactionReq;
@@ -74,10 +76,9 @@ sentryTest(
await waitForReplayRunning(page);
await page.evaluate(() => {
- (window as unknown as TestWindow).Sentry.configureScope(scope => {
- scope.setUser({ id: 'user123', segment: 'segmentB' });
- scope.setTransactionName('testTransactionDSC');
- });
+ const scope = (window as unknown as TestWindow).Sentry.getCurrentScope();
+ scope.setUser({ id: 'user123', segment: 'segmentB' });
+ scope.setTransactionName('testTransactionDSC');
});
const req0 = await transactionReq;
@@ -132,10 +133,9 @@ sentryTest(
});
await page.evaluate(() => {
- (window as unknown as TestWindow).Sentry.configureScope(scope => {
- scope.setUser({ id: 'user123', segment: 'segmentB' });
- scope.setTransactionName('testTransactionDSC');
- });
+ const scope = (window as unknown as TestWindow).Sentry.getCurrentScope();
+ scope.setUser({ id: 'user123', segment: 'segmentB' });
+ scope.setTransactionName('testTransactionDSC');
});
const req0 = await transactionReq;
@@ -181,10 +181,9 @@ sentryTest(
const transactionReq = waitForTransactionRequest(page);
await page.evaluate(async () => {
- (window as unknown as TestWindow).Sentry.configureScope(scope => {
- scope.setUser({ id: 'user123', segment: 'segmentB' });
- scope.setTransactionName('testTransactionDSC');
- });
+ const scope = (window as unknown as TestWindow).Sentry.getCurrentScope();
+ scope.setUser({ id: 'user123', segment: 'segmentB' });
+ scope.setTransactionName('testTransactionDSC');
});
const req0 = await transactionReq;
diff --git a/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js b/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js
index efb7b577f75b..7d000c0ac2cd 100644
--- a/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js
+++ b/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js
@@ -11,8 +11,7 @@ Sentry.init({
debug: true,
});
-Sentry.configureScope(scope => {
- scope.setUser({ id: 'user123', segment: 'segmentB' });
- scope.setTransactionName('testTransactionDSC');
- scope.getTransaction().setMetadata({ source: 'custom' });
-});
+const scope = Sentry.getCurrentScope();
+scope.setUser({ id: 'user123', segment: 'segmentB' });
+scope.setTransactionName('testTransactionDSC');
+scope.getTransaction().setMetadata({ source: 'custom' });
diff --git a/packages/browser-integration-tests/suites/tracing/envelope-header/init.js b/packages/browser-integration-tests/suites/tracing/envelope-header/init.js
index fbce5a16116a..f382a49c153d 100644
--- a/packages/browser-integration-tests/suites/tracing/envelope-header/init.js
+++ b/packages/browser-integration-tests/suites/tracing/envelope-header/init.js
@@ -11,7 +11,6 @@ Sentry.init({
debug: true,
});
-Sentry.configureScope(scope => {
- scope.setUser({ id: 'user123', segment: 'segmentB' });
- scope.setTransactionName('testTransactionDSC');
-});
+const scope = Sentry.getCurrentScope();
+scope.setUser({ id: 'user123', segment: 'segmentB' });
+scope.setTransactionName('testTransactionDSC');
diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts
index e361f1366cf3..6955fbfa26fe 100644
--- a/packages/browser/src/eventbuilder.ts
+++ b/packages/browser/src/eventbuilder.ts
@@ -1,4 +1,4 @@
-import { getCurrentHub } from '@sentry/core';
+import { getClient } from '@sentry/core';
import type { Event, EventHint, Exception, Severity, SeverityLevel, StackFrame, StackParser } from '@sentry/types';
import {
addExceptionMechanism,
@@ -48,8 +48,7 @@ export function eventFromPlainObject(
syntheticException?: Error,
isUnhandledRejection?: boolean,
): Event {
- const hub = getCurrentHub();
- const client = hub.getClient();
+ const client = getClient();
const normalizeDepth = client && client.getOptions().normalizeDepth;
const event: Event = {
diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts
index 82eedf4e846f..cc9712517b2b 100644
--- a/packages/browser/src/exports.ts
+++ b/packages/browser/src/exports.ts
@@ -30,6 +30,7 @@ export {
captureEvent,
captureMessage,
close,
+ // eslint-disable-next-line deprecation/deprecation
configureScope,
createTransport,
flush,
diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts
index cfcb255f5999..9c6b4cfb9764 100644
--- a/packages/browser/src/integrations/breadcrumbs.ts
+++ b/packages/browser/src/integrations/breadcrumbs.ts
@@ -1,5 +1,5 @@
/* eslint-disable max-lines */
-import { getClient, getCurrentHub } from '@sentry/core';
+import { addBreadcrumb, getClient } from '@sentry/core';
import type {
Event as SentryEvent,
HandlerDataConsole,
@@ -123,7 +123,7 @@ export class Breadcrumbs implements Integration {
* Adds a breadcrumb for Sentry events or transactions if this option is enabled.
*/
function addSentryBreadcrumb(event: SentryEvent): void {
- getCurrentHub().addBreadcrumb(
+ addBreadcrumb(
{
category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`,
event_id: event.event_id,
@@ -173,7 +173,7 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa
return;
}
- getCurrentHub().addBreadcrumb(
+ addBreadcrumb(
{
category: `ui.${handlerData.name}`,
message: target,
@@ -213,7 +213,7 @@ function _consoleBreadcrumb(handlerData: HandlerDataConsole): void {
}
}
- getCurrentHub().addBreadcrumb(breadcrumb, {
+ addBreadcrumb(breadcrumb, {
input: handlerData.args,
level: handlerData.level,
});
@@ -247,7 +247,7 @@ function _xhrBreadcrumb(handlerData: HandlerDataXhr): void {
endTimestamp,
};
- getCurrentHub().addBreadcrumb(
+ addBreadcrumb(
{
category: 'xhr',
data,
@@ -282,7 +282,7 @@ function _fetchBreadcrumb(handlerData: HandlerDataFetch): void {
endTimestamp,
};
- getCurrentHub().addBreadcrumb(
+ addBreadcrumb(
{
category: 'fetch',
data,
@@ -303,7 +303,7 @@ function _fetchBreadcrumb(handlerData: HandlerDataFetch): void {
startTimestamp,
endTimestamp,
};
- getCurrentHub().addBreadcrumb(
+ addBreadcrumb(
{
category: 'fetch',
data,
@@ -338,7 +338,7 @@ function _historyBreadcrumb(handlerData: HandlerDataHistory): void {
from = parsedFrom.relative;
}
- getCurrentHub().addBreadcrumb({
+ addBreadcrumb({
category: 'navigation',
data: {
from,
diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts
index 0c3f3be60e2d..079ef6083212 100644
--- a/packages/browser/src/integrations/globalhandlers.ts
+++ b/packages/browser/src/integrations/globalhandlers.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
-import { getCurrentHub } from '@sentry/core';
-import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types';
+import { captureEvent, getClient } from '@sentry/core';
+import type { Client, Event, Integration, Primitive, StackParser } from '@sentry/types';
import {
addGlobalErrorInstrumentationHandler,
addGlobalUnhandledRejectionInstrumentationHandler,
@@ -36,12 +36,6 @@ export class GlobalHandlers implements Integration {
/** JSDoc */
private readonly _options: GlobalHandlersIntegrations;
- /**
- * Stores references functions to installing handlers. Will set to undefined
- * after they have been run so that they are not used twice.
- */
- private _installFunc: Record void) | undefined>;
-
/** JSDoc */
public constructor(options?: GlobalHandlersIntegrations) {
this.name = GlobalHandlers.id;
@@ -50,43 +44,36 @@ export class GlobalHandlers implements Integration {
onunhandledrejection: true,
...options,
};
-
- this._installFunc = {
- onerror: _installGlobalOnErrorHandler,
- onunhandledrejection: _installGlobalOnUnhandledRejectionHandler,
- };
}
/**
* @inheritDoc
*/
public setupOnce(): void {
Error.stackTraceLimit = 50;
- const options = this._options;
-
- // We can disable guard-for-in as we construct the options object above + do checks against
- // `this._installFunc` for the property.
- // eslint-disable-next-line guard-for-in
- for (const key in options) {
- const installFunc = this._installFunc[key as GlobalHandlersIntegrationsOptionKeys];
- if (installFunc && options[key as GlobalHandlersIntegrationsOptionKeys]) {
- globalHandlerLog(key);
- installFunc();
- this._installFunc[key as GlobalHandlersIntegrationsOptionKeys] = undefined;
- }
+ }
+
+ /** @inheritdoc */
+ public setup(client: Client): void {
+ if (this._options.onerror) {
+ _installGlobalOnErrorHandler(client);
+ globalHandlerLog('onerror');
+ }
+ if (this._options.onunhandledrejection) {
+ _installGlobalOnUnhandledRejectionHandler(client);
+ globalHandlerLog('onunhandledrejection');
}
}
}
-function _installGlobalOnErrorHandler(): void {
+function _installGlobalOnErrorHandler(client: Client): void {
addGlobalErrorInstrumentationHandler(data => {
- const [hub, stackParser, attachStacktrace] = getHubAndOptions();
- if (!hub.getIntegration(GlobalHandlers)) {
+ const { stackParser, attachStacktrace } = getOptions();
+
+ if (getClient() !== client || shouldIgnoreOnError()) {
return;
}
+
const { msg, url, line, column, error } = data;
- if (shouldIgnoreOnError()) {
- return;
- }
const event =
error === undefined && isString(msg)
@@ -100,7 +87,7 @@ function _installGlobalOnErrorHandler(): void {
event.level = 'error';
- hub.captureEvent(event, {
+ captureEvent(event, {
originalException: error,
mechanism: {
handled: false,
@@ -110,15 +97,12 @@ function _installGlobalOnErrorHandler(): void {
});
}
-function _installGlobalOnUnhandledRejectionHandler(): void {
+function _installGlobalOnUnhandledRejectionHandler(client: Client): void {
addGlobalUnhandledRejectionInstrumentationHandler(e => {
- const [hub, stackParser, attachStacktrace] = getHubAndOptions();
- if (!hub.getIntegration(GlobalHandlers)) {
- return;
- }
+ const { stackParser, attachStacktrace } = getOptions();
- if (shouldIgnoreOnError()) {
- return true;
+ if (getClient() !== client || shouldIgnoreOnError()) {
+ return;
}
const error = _getUnhandledRejectionError(e as unknown);
@@ -129,15 +113,13 @@ function _installGlobalOnUnhandledRejectionHandler(): void {
event.level = 'error';
- hub.captureEvent(event, {
+ captureEvent(event, {
originalException: error,
mechanism: {
handled: false,
type: 'onunhandledrejection',
},
});
-
- return;
});
}
@@ -258,12 +240,11 @@ function globalHandlerLog(type: string): void {
DEBUG_BUILD && logger.log(`Global Handler attached: ${type}`);
}
-function getHubAndOptions(): [Hub, StackParser, boolean | undefined] {
- const hub = getCurrentHub();
- const client = hub.getClient();
+function getOptions(): { stackParser: StackParser; attachStacktrace?: boolean } {
+ const client = getClient();
const options = (client && client.getOptions()) || {
stackParser: () => [],
attachStacktrace: false,
};
- return [hub, options.stackParser, options.attachStacktrace];
+ return options;
}
diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts
index 326af29492cf..5173705feaa6 100644
--- a/packages/browser/src/profiling/integration.ts
+++ b/packages/browser/src/profiling/integration.ts
@@ -1,4 +1,5 @@
-import type { EventEnvelope, EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
+import { getCurrentScope } from '@sentry/core';
+import type { Client, EventEnvelope, EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import type { Profile } from '@sentry/types/src/profiling';
import { logger } from '@sentry/utils';
@@ -29,6 +30,7 @@ export class BrowserProfilingIntegration implements Integration {
public readonly name: string;
+ /** @deprecated This is never set. */
public getCurrentHub?: () => Hub;
public constructor() {
@@ -38,12 +40,13 @@ export class BrowserProfilingIntegration implements Integration {
/**
* @inheritDoc
*/
- public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
- this.getCurrentHub = getCurrentHub;
+ public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void {
+ // noop
+ }
- const hub = this.getCurrentHub();
- const client = hub.getClient();
- const scope = hub.getScope();
+ /** @inheritdoc */
+ public setup(client: Client): void {
+ const scope = getCurrentScope();
const transaction = scope.getTransaction();
@@ -53,67 +56,68 @@ export class BrowserProfilingIntegration implements Integration {
}
}
- if (client && typeof client.on === 'function') {
- client.on('startTransaction', (transaction: Transaction) => {
- if (shouldProfileTransaction(transaction)) {
- startProfileForTransaction(transaction);
+ if (typeof client.on !== 'function') {
+ logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
+ return;
+ }
+
+ client.on('startTransaction', (transaction: Transaction) => {
+ if (shouldProfileTransaction(transaction)) {
+ startProfileForTransaction(transaction);
+ }
+ });
+
+ client.on('beforeEnvelope', (envelope): void => {
+ // if not profiles are in queue, there is nothing to add to the envelope.
+ if (!getActiveProfilesCount()) {
+ return;
+ }
+
+ const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
+ if (!profiledTransactionEvents.length) {
+ return;
+ }
+
+ const profilesToAddToEnvelope: Profile[] = [];
+
+ for (const profiledTransaction of profiledTransactionEvents) {
+ const context = profiledTransaction && profiledTransaction.contexts;
+ const profile_id = context && context['profile'] && context['profile']['profile_id'];
+ const start_timestamp = context && context['profile'] && context['profile']['start_timestamp'];
+
+ if (typeof profile_id !== 'string') {
+ DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context');
+ continue;
}
- });
- client.on('beforeEnvelope', (envelope): void => {
- // if not profiles are in queue, there is nothing to add to the envelope.
- if (!getActiveProfilesCount()) {
- return;
+ if (!profile_id) {
+ DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context');
+ continue;
}
- const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
- if (!profiledTransactionEvents.length) {
- return;
+ // Remove the profile from the transaction context before sending, relay will take care of the rest.
+ if (context && context['profile']) {
+ delete context.profile;
}
- const profilesToAddToEnvelope: Profile[] = [];
-
- for (const profiledTransaction of profiledTransactionEvents) {
- const context = profiledTransaction && profiledTransaction.contexts;
- const profile_id = context && context['profile'] && context['profile']['profile_id'];
- const start_timestamp = context && context['profile'] && context['profile']['start_timestamp'];
-
- if (typeof profile_id !== 'string') {
- DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context');
- continue;
- }
-
- if (!profile_id) {
- DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context');
- continue;
- }
-
- // Remove the profile from the transaction context before sending, relay will take care of the rest.
- if (context && context['profile']) {
- delete context.profile;
- }
-
- const profile = takeProfileFromGlobalCache(profile_id);
- if (!profile) {
- DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
- continue;
- }
-
- const profileEvent = createProfilingEvent(
- profile_id,
- start_timestamp as number | undefined,
- profile,
- profiledTransaction as ProfiledEvent,
- );
- if (profileEvent) {
- profilesToAddToEnvelope.push(profileEvent);
- }
+ const profile = takeProfileFromGlobalCache(profile_id);
+ if (!profile) {
+ DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
+ continue;
}
- addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope);
- });
- } else {
- logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');
- }
+ const profileEvent = createProfilingEvent(
+ profile_id,
+ start_timestamp as number | undefined,
+ profile,
+ profiledTransaction as ProfiledEvent,
+ );
+ if (profileEvent) {
+ profilesToAddToEnvelope.push(profileEvent);
+ }
+ }
+
+ addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope);
+ });
}
}
diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts
index 3edb82e0b539..f2fdc5e4c10d 100644
--- a/packages/browser/src/profiling/utils.ts
+++ b/packages/browser/src/profiling/utils.ts
@@ -1,6 +1,6 @@
/* eslint-disable max-lines */
-import { DEFAULT_ENVIRONMENT, getClient, getCurrentHub } from '@sentry/core';
+import { DEFAULT_ENVIRONMENT, getClient } from '@sentry/core';
import type { DebugImage, Envelope, Event, EventEnvelope, StackFrame, StackParser, Transaction } from '@sentry/types';
import type { Profile, ThreadCpuProfile } from '@sentry/types/src/profiling';
import { GLOBAL_OBJ, browserPerformanceTimeOrigin, forEachEnvelopeItem, logger, uuid4 } from '@sentry/utils';
@@ -347,19 +347,10 @@ export function applyDebugMetadata(resource_paths: ReadonlyArray): Debug
return [];
}
- const hub = getCurrentHub();
- if (!hub) {
- return [];
- }
- const client = hub.getClient();
- if (!client) {
- return [];
- }
- const options = client.getOptions();
- if (!options) {
- return [];
- }
- const stackParser = options.stackParser;
+ const client = getClient();
+ const options = client && client.getOptions();
+ const stackParser = options && options.stackParser;
+
if (!stackParser) {
return [];
}
diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts
index bc0058ba7d16..5968349adc1a 100644
--- a/packages/browser/test/unit/index.test.ts
+++ b/packages/browser/test/unit/index.test.ts
@@ -12,10 +12,10 @@ import {
captureEvent,
captureException,
captureMessage,
- configureScope,
flush,
getClient,
getCurrentHub,
+ getCurrentScope,
init,
showReportDialog,
wrap,
@@ -37,9 +37,10 @@ jest.mock('@sentry/core', () => {
});
describe('SentryBrowser', () => {
- const beforeSend = jest.fn();
+ const beforeSend = jest.fn(event => event);
- beforeAll(() => {
+ beforeEach(() => {
+ WINDOW.__SENTRY__ = { hub: undefined, logger: undefined, globalEventProcessors: [] };
init({
beforeSend,
dsn,
@@ -47,39 +48,28 @@ describe('SentryBrowser', () => {
});
});
- beforeEach(() => {
- getCurrentHub().pushScope();
- });
-
afterEach(() => {
- getCurrentHub().popScope();
- beforeSend.mockReset();
+ beforeSend.mockClear();
});
describe('getContext() / setContext()', () => {
it('should store/load extra', () => {
- configureScope((scope: Scope) => {
- scope.setExtra('abc', { def: [1] });
- });
- expect(global.__SENTRY__.hub._stack[1].scope._extra).toEqual({
+ getCurrentScope().setExtra('abc', { def: [1] });
+ expect(global.__SENTRY__.hub._stack[0].scope._extra).toEqual({
abc: { def: [1] },
});
});
it('should store/load tags', () => {
- configureScope((scope: Scope) => {
- scope.setTag('abc', 'def');
- });
- expect(global.__SENTRY__.hub._stack[1].scope._tags).toEqual({
+ getCurrentScope().setTag('abc', 'def');
+ expect(global.__SENTRY__.hub._stack[0].scope._tags).toEqual({
abc: 'def',
});
});
it('should store/load user', () => {
- configureScope((scope: Scope) => {
- scope.setUser({ id: 'def' });
- });
- expect(global.__SENTRY__.hub._stack[1].scope._user).toEqual({
+ getCurrentScope().setUser({ id: 'def' });
+ expect(global.__SENTRY__.hub._stack[0].scope._user).toEqual({
id: 'def',
});
});
@@ -95,9 +85,7 @@ describe('SentryBrowser', () => {
const options = getDefaultBrowserClientOptions({ dsn });
const client = new BrowserClient(options);
it('uses the user on the scope', () => {
- configureScope(scope => {
- scope.setUser(EX_USER);
- });
+ getCurrentScope().setUser(EX_USER);
getCurrentHub().bindClient(client);
showReportDialog();
@@ -110,9 +98,7 @@ describe('SentryBrowser', () => {
});
it('prioritizes options user over scope user', () => {
- configureScope(scope => {
- scope.setUser(EX_USER);
- });
+ getCurrentScope().setUser(EX_USER);
getCurrentHub().bindClient(client);
const DIALOG_OPTION_USER = { email: 'option@example.com' };
diff --git a/packages/browser/test/unit/integrations/breadcrumbs.test.ts b/packages/browser/test/unit/integrations/breadcrumbs.test.ts
index d81107a69c38..e87454737482 100644
--- a/packages/browser/test/unit/integrations/breadcrumbs.test.ts
+++ b/packages/browser/test/unit/integrations/breadcrumbs.test.ts
@@ -1,4 +1,4 @@
-import { getCurrentHub } from '@sentry/core';
+import * as SentryCore from '@sentry/core';
import type { Client } from '@sentry/types';
import { Breadcrumbs, BrowserClient, Hub, flush } from '../../../src';
@@ -18,21 +18,20 @@ jest.mock('@sentry/core', () => {
describe('Breadcrumbs', () => {
it('Should add sentry breadcrumb', async () => {
- const addBreadcrumb = jest.fn();
- hub.addBreadcrumb = addBreadcrumb;
-
client = new BrowserClient({
...getDefaultBrowserClientOptions(),
dsn: 'https://username@domain/123',
integrations: [new Breadcrumbs()],
});
- getCurrentHub().bindClient(client);
+ SentryCore.getCurrentHub().bindClient(client);
+
+ const addBreadcrumbSpy = jest.spyOn(SentryCore, 'addBreadcrumb').mockImplementation(() => {});
client.captureMessage('test');
await flush(2000);
- expect(addBreadcrumb.mock.calls[0][0].category).toEqual('sentry.event');
- expect(addBreadcrumb.mock.calls[0][0].message).toEqual('test');
+ expect(addBreadcrumbSpy.mock.calls[0][0].category).toEqual('sentry.event');
+ expect(addBreadcrumbSpy.mock.calls[0][0].message).toEqual('test');
});
});
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index 5a4260aaec38..499e969f3843 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -33,6 +33,7 @@ export {
captureEvent,
captureMessage,
close,
+ // eslint-disable-next-line deprecation/deprecation
configureScope,
createTransport,
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts
index a6e0575a2e67..3fbdd13250f8 100644
--- a/packages/core/src/baseclient.ts
+++ b/packages/core/src/baseclient.ts
@@ -48,7 +48,7 @@ import {
import { getEnvelopeEndpointWithUrlEncodedAuth } from './api';
import { DEBUG_BUILD } from './debug-build';
import { createEventEnvelope, createSessionEnvelope } from './envelope';
-import { getCurrentHub } from './hub';
+import { getClient } from './exports';
import type { IntegrationIndex } from './integration';
import { setupIntegration, setupIntegrations } from './integration';
import { createMetricEnvelope } from './metrics/envelope';
@@ -115,14 +115,14 @@ export abstract class BaseClient implements Client {
/** Number of calls being processed */
protected _numProcessing: number;
+ protected _eventProcessors: EventProcessor[];
+
/** Holds flushable */
private _outcomes: { [key: string]: number };
// eslint-disable-next-line @typescript-eslint/ban-types
private _hooks: Record;
- private _eventProcessors: EventProcessor[];
-
/**
* Initializes this client instance.
*
@@ -870,7 +870,7 @@ function isTransactionEvent(event: Event): event is TransactionEvent {
* This event processor will run for all events processed by this client.
*/
export function addEventProcessor(callback: EventProcessor): void {
- const client = getCurrentHub().getClient();
+ const client = getClient();
if (!client || !client.addEventProcessor) {
return;
diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts
index 6f71e7dfbccb..95c1e4b63de3 100644
--- a/packages/core/src/exports.ts
+++ b/packages/core/src/exports.ts
@@ -1,5 +1,6 @@
import type {
Breadcrumb,
+ BreadcrumbHint,
CaptureContext,
CheckIn,
Client,
@@ -77,8 +78,11 @@ export function captureEvent(event: Event, hint?: EventHint): ReturnType void): ReturnType {
+ // eslint-disable-next-line deprecation/deprecation
getCurrentHub().configureScope(callback);
}
@@ -90,8 +94,8 @@ export function configureScope(callback: (scope: Scope) => void): ReturnType {
- getCurrentHub().addBreadcrumb(breadcrumb);
+export function addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): ReturnType {
+ getCurrentHub().addBreadcrumb(breadcrumb, hint);
}
/**
@@ -163,8 +167,8 @@ export function setUser(user: User | null): ReturnType {
*
* @param callback that will be enclosed into push/popScope.
*/
-export function withScope(callback: (scope: Scope) => void): ReturnType {
- getCurrentHub().withScope(callback);
+export function withScope(callback: (scope: Scope) => T): T {
+ return getCurrentHub().withScope(callback);
}
/**
@@ -202,9 +206,8 @@ export function startTransaction(
* to create a monitor automatically when sending a check in.
*/
export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string {
- const hub = getCurrentHub();
- const scope = hub.getScope();
- const client = hub.getClient();
+ const scope = getCurrentScope();
+ const client = getClient();
if (!client) {
DEBUG_BUILD && logger.warn('Cannot capture check-in. No client defined.');
} else if (!client.captureCheckIn) {
diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts
index 13d5fd059e93..75960550081a 100644
--- a/packages/core/src/hub.ts
+++ b/packages/core/src/hub.ts
@@ -139,6 +139,8 @@ export class Hub implements HubInterface {
/**
* @inheritDoc
+ *
+ * @deprecated Use `withScope` instead.
*/
public pushScope(): Scope {
// We want to clone the content of prev scope
@@ -152,6 +154,8 @@ export class Hub implements HubInterface {
/**
* @inheritDoc
+ *
+ * @deprecated Use `withScope` instead.
*/
public popScope(): boolean {
if (this.getStack().length <= 1) return false;
@@ -161,11 +165,13 @@ export class Hub implements HubInterface {
/**
* @inheritDoc
*/
- public withScope(callback: (scope: Scope) => void): void {
+ public withScope(callback: (scope: Scope) => T): T {
+ // eslint-disable-next-line deprecation/deprecation
const scope = this.pushScope();
try {
- callback(scope);
+ return callback(scope);
} finally {
+ // eslint-disable-next-line deprecation/deprecation
this.popScope();
}
}
@@ -335,6 +341,8 @@ export class Hub implements HubInterface {
/**
* @inheritDoc
+ *
+ * @deprecated Use `getScope()` directly.
*/
public configureScope(callback: (scope: Scope) => void): void {
const { scope, client } = this.getStackTop();
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index ccf219031b8f..6468c312bbe1 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -14,6 +14,7 @@ export {
captureEvent,
captureMessage,
close,
+ // eslint-disable-next-line deprecation/deprecation
configureScope,
flush,
lastEventId,
@@ -43,6 +44,7 @@ export { makeSession, closeSession, updateSession } from './session';
export { SessionFlusher } from './sessionflusher';
export { Scope } from './scope';
export {
+ notifyEventProcessors,
// eslint-disable-next-line deprecation/deprecation
addGlobalEventProcessor,
} from './eventProcessors';
@@ -54,7 +56,12 @@ export { createTransport } from './transports/base';
export { makeOfflineTransport } from './transports/offline';
export { makeMultiplexedTransport } from './transports/multiplexed';
export { SDK_VERSION } from './version';
-export { getIntegrationsToSetup, addIntegration } from './integration';
+export {
+ getIntegrationsToSetup,
+ addIntegration,
+ // eslint-disable-next-line deprecation/deprecation
+ convertIntegrationFnToClass,
+} from './integration';
export { FunctionToString, InboundFilters, LinkedErrors } from './integrations';
export { prepareEvent } from './utils/prepareEvent';
export { createCheckInEnvelope } from './checkin';
diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts
index 7be22a316e53..b7782fcfa65c 100644
--- a/packages/core/src/integration.ts
+++ b/packages/core/src/integration.ts
@@ -1,4 +1,4 @@
-import type { Client, Event, EventHint, Integration, Options } from '@sentry/types';
+import type { Client, Event, EventHint, Integration, IntegrationClass, IntegrationFn, Options } from '@sentry/types';
import { arrayify, logger } from '@sentry/utils';
import { DEBUG_BUILD } from './debug-build';
@@ -46,7 +46,7 @@ function filterDuplicates(integrations: Integration[]): Integration[] {
}
/** Gets integrations to install */
-export function getIntegrationsToSetup(options: Options): Integration[] {
+export function getIntegrationsToSetup(options: Pick): Integration[] {
const defaultIntegrations = options.defaultIntegrations || [];
const userIntegrations = options.integrations;
@@ -155,3 +155,26 @@ function findIndex(arr: T[], callback: (item: T) => boolean): number {
return -1;
}
+
+/**
+ * Convert a new integration function to the legacy class syntax.
+ * In v8, we can remove this and instead export the integration functions directly.
+ *
+ * @deprecated This will be removed in v8!
+ */
+export function convertIntegrationFnToClass(
+ name: string,
+ fn: Fn,
+): IntegrationClass {
+ return Object.assign(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ function ConvertedIntegration(...rest: any[]) {
+ return {
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ setupOnce: () => {},
+ ...fn(...rest),
+ };
+ },
+ { id: name },
+ ) as unknown as IntegrationClass;
+}
diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts
index 9d348a4b4d23..57c0387b25e4 100644
--- a/packages/core/src/integrations/inboundfilters.ts
+++ b/packages/core/src/integrations/inboundfilters.ts
@@ -1,7 +1,8 @@
-import type { Client, Event, EventHint, Integration, StackFrame } from '@sentry/types';
+import type { Event, IntegrationFn, StackFrame } from '@sentry/types';
import { getEventDescription, logger, stringMatchesSomePattern } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
+import { convertIntegrationFnToClass } from '../integration';
// "Script error." is hard coded into browsers for errors that it can't read.
// this is the result of a script being pulled in from an external domain and CORS.
@@ -28,42 +29,23 @@ export interface InboundFiltersOptions {
disableTransactionDefaults: boolean;
}
-/** Inbound filters configurable by the user */
-export class InboundFilters implements Integration {
- /**
- * @inheritDoc
- */
- public static id: string = 'InboundFilters';
-
- /**
- * @inheritDoc
- */
- public name: string;
-
- private readonly _options: Partial;
-
- public constructor(options: Partial = {}) {
- this.name = InboundFilters.id;
- this._options = options;
- }
-
- /**
- * @inheritDoc
- */
- public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void {
- // noop
- }
+const INTEGRATION_NAME = 'InboundFilters';
+const inboundFiltersIntegration: IntegrationFn = (options: Partial) => {
+ return {
+ name: INTEGRATION_NAME,
+ processEvent(event, _hint, client) {
+ const clientOptions = client.getOptions();
+ const mergedOptions = _mergeOptions(options, clientOptions);
+ return _shouldDropEvent(event, mergedOptions) ? null : event;
+ },
+ };
+};
- /** @inheritDoc */
- public processEvent(event: Event, _eventHint: EventHint, client: Client): Event | null {
- const clientOptions = client.getOptions();
- const options = _mergeOptions(this._options, clientOptions);
- return _shouldDropEvent(event, options) ? null : event;
- }
-}
+/** Inbound filters configurable by the user */
+// eslint-disable-next-line deprecation/deprecation
+export const InboundFilters = convertIntegrationFnToClass(INTEGRATION_NAME, inboundFiltersIntegration);
-/** JSDoc */
-export function _mergeOptions(
+function _mergeOptions(
internalOptions: Partial = {},
clientOptions: Partial = {},
): Partial {
@@ -84,8 +66,7 @@ export function _mergeOptions(
};
}
-/** JSDoc */
-export function _shouldDropEvent(event: Event, options: Partial): boolean {
+function _shouldDropEvent(event: Event, options: Partial): boolean {
if (options.ignoreInternal && _isSentryError(event)) {
DEBUG_BUILD &&
logger.warn(`Event dropped due to being internal Sentry Error.\nEvent: ${getEventDescription(event)}`);
diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts
index c27e76cf79b1..22a5e83ffb3d 100644
--- a/packages/core/src/metrics/exports.ts
+++ b/packages/core/src/metrics/exports.ts
@@ -2,7 +2,7 @@ import type { ClientOptions, MeasurementUnit, Primitive } from '@sentry/types';
import { logger } from '@sentry/utils';
import type { BaseClient } from '../baseclient';
import { DEBUG_BUILD } from '../debug-build';
-import { getCurrentHub } from '../hub';
+import { getClient, getCurrentScope } from '../exports';
import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants';
import { MetricsAggregator } from './integration';
import type { MetricType } from './types';
@@ -19,9 +19,8 @@ function addToMetricsAggregator(
value: number | string,
data: MetricData = {},
): void {
- const hub = getCurrentHub();
- const client = hub.getClient() as BaseClient;
- const scope = hub.getScope();
+ const client = getClient>();
+ const scope = getCurrentScope();
if (client) {
if (!client.metricsAggregator) {
DEBUG_BUILD &&
diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts
index 4165aec8fc46..719e2b81f086 100644
--- a/packages/core/src/server-runtime-client.ts
+++ b/packages/core/src/server-runtime-client.ts
@@ -16,7 +16,7 @@ import { eventFromMessage, eventFromUnknownInput, logger, resolvedSyncPromise, u
import { BaseClient } from './baseclient';
import { createCheckInEnvelope } from './checkin';
import { DEBUG_BUILD } from './debug-build';
-import { getCurrentHub } from './hub';
+import { getClient } from './exports';
import type { Scope } from './scope';
import { SessionFlusher } from './sessionflusher';
import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing';
@@ -50,7 +50,7 @@ export class ServerRuntimeClient<
* @inheritDoc
*/
public eventFromException(exception: unknown, hint?: EventHint): PromiseLike {
- return resolvedSyncPromise(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint));
+ return resolvedSyncPromise(eventFromUnknownInput(getClient(), this._options.stackParser, exception, hint));
}
/**
diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts
index 0b0bc8455480..dac81b82336d 100644
--- a/packages/core/src/sessionflusher.ts
+++ b/packages/core/src/sessionflusher.ts
@@ -6,8 +6,7 @@ import type {
SessionFlusherLike,
} from '@sentry/types';
import { dropUndefinedKeys } from '@sentry/utils';
-
-import { getCurrentHub } from './hub';
+import { getCurrentScope } from './exports';
type ReleaseHealthAttributes = {
environment?: string;
@@ -75,7 +74,7 @@ export class SessionFlusher implements SessionFlusherLike {
if (!this._isEnabled) {
return;
}
- const scope = getCurrentHub().getScope();
+ const scope = getCurrentScope();
const requestSession = scope.getRequestSession();
if (requestSession && requestSession.status) {
diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts
index b49b1d15e9b1..75630de373f1 100644
--- a/packages/core/src/tracing/idletransaction.ts
+++ b/packages/core/src/tracing/idletransaction.ts
@@ -121,7 +121,7 @@ export class IdleTransaction extends Transaction {
// We set the transaction here on the scope so error events pick up the trace
// context and attach it to the error.
DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanId}`);
- _idleHub.configureScope(scope => scope.setSpan(this));
+ _idleHub.getScope().setSpan(this);
}
this._restartIdleTimeout();
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index 667bedaaef6c..cc73fe009e3d 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -2,6 +2,7 @@ import type { TransactionContext } from '@sentry/types';
import { dropUndefinedKeys, isThenable, logger, tracingContextFromHeaders } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build';
+import { getCurrentScope } from '../exports';
import type { Hub } from '../hub';
import { getCurrentHub } from '../hub';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
@@ -23,12 +24,14 @@ export function trace(
context: TransactionContext,
callback: (span?: Span) => T,
// eslint-disable-next-line @typescript-eslint/no-empty-function
- onError: (error: unknown) => void = () => {},
+ onError: (error: unknown, span?: Span) => void = () => {},
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ afterFinish: () => void = () => {},
): T {
const ctx = normalizeContext(context);
const hub = getCurrentHub();
- const scope = hub.getScope();
+ const scope = getCurrentScope();
const parentSpan = scope.getSpan();
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
@@ -37,7 +40,7 @@ export function trace(
function finishAndSetSpan(): void {
activeSpan && activeSpan.finish();
- hub.getScope().setSpan(parentSpan);
+ scope.setSpan(parentSpan);
}
let maybePromiseResult: T;
@@ -45,8 +48,9 @@ export function trace(
maybePromiseResult = callback(activeSpan);
} catch (e) {
activeSpan && activeSpan.setStatus('internal_error');
- onError(e);
+ onError(e, activeSpan);
finishAndSetSpan();
+ afterFinish();
throw e;
}
@@ -54,15 +58,18 @@ export function trace(
Promise.resolve(maybePromiseResult).then(
() => {
finishAndSetSpan();
+ afterFinish();
},
e => {
activeSpan && activeSpan.setStatus('internal_error');
- onError(e);
+ onError(e, activeSpan);
finishAndSetSpan();
+ afterFinish();
},
);
} else {
finishAndSetSpan();
+ afterFinish();
}
return maybePromiseResult;
@@ -83,7 +90,7 @@ export function startSpan(context: TransactionContext, callback: (span: Span
const ctx = normalizeContext(context);
const hub = getCurrentHub();
- const scope = hub.getScope();
+ const scope = getCurrentScope();
const parentSpan = scope.getSpan();
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
@@ -91,7 +98,7 @@ export function startSpan(context: TransactionContext, callback: (span: Span
function finishAndSetSpan(): void {
activeSpan && activeSpan.finish();
- hub.getScope().setSpan(parentSpan);
+ scope.setSpan(parentSpan);
}
let maybePromiseResult: T;
@@ -143,7 +150,7 @@ export function startSpanManual(
const ctx = normalizeContext(context);
const hub = getCurrentHub();
- const scope = hub.getScope();
+ const scope = getCurrentScope();
const parentSpan = scope.getSpan();
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
@@ -151,7 +158,7 @@ export function startSpanManual(
function finishAndSetSpan(): void {
activeSpan && activeSpan.finish();
- hub.getScope().setSpan(parentSpan);
+ scope.setSpan(parentSpan);
}
let maybePromiseResult: T;
@@ -201,7 +208,7 @@ export function startInactiveSpan(context: TransactionContext): Span | undefined
* Returns the currently active span.
*/
export function getActiveSpan(): Span | undefined {
- return getCurrentHub().getScope().getSpan();
+ return getCurrentScope().getSpan();
}
export function continueTrace({
@@ -238,8 +245,7 @@ export function continueTrace(
},
callback?: (transactionContext: Partial) => V,
): V | Partial {
- const hub = getCurrentHub();
- const currentScope = hub.getScope();
+ const currentScope = getCurrentScope();
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
sentryTrace,
diff --git a/packages/core/src/utils/isSentryRequestUrl.ts b/packages/core/src/utils/isSentryRequestUrl.ts
index 0256e3cf7835..3a31f63cf46c 100644
--- a/packages/core/src/utils/isSentryRequestUrl.ts
+++ b/packages/core/src/utils/isSentryRequestUrl.ts
@@ -1,11 +1,13 @@
-import type { DsnComponents, Hub } from '@sentry/types';
+import type { Client, DsnComponents, Hub } from '@sentry/types';
/**
* Checks whether given url points to Sentry server
* @param url url to verify
+ *
+ * TODO(v8): Remove Hub fallback type
*/
-export function isSentryRequestUrl(url: string, hub: Hub): boolean {
- const client = hub.getClient();
+export function isSentryRequestUrl(url: string, hubOrClient: Hub | Client | undefined): boolean {
+ const client = hubOrClient && isHub(hubOrClient) ? hubOrClient.getClient() : hubOrClient;
const dsn = client && client.getDsn();
const tunnel = client && client.getOptions().tunnel;
@@ -27,3 +29,7 @@ function checkDsn(url: string, dsn: DsnComponents | undefined): boolean {
function removeTrailingSlash(str: string): string {
return str[str.length - 1] === '/' ? str.slice(0, -1) : str;
}
+
+function isHub(hubOrClient: Hub | Client | undefined): hubOrClient is Hub {
+ return (hubOrClient as Hub).getClient !== undefined;
+}
diff --git a/packages/core/test/lib/exports.test.ts b/packages/core/test/lib/exports.test.ts
new file mode 100644
index 000000000000..89b4fd9105d5
--- /dev/null
+++ b/packages/core/test/lib/exports.test.ts
@@ -0,0 +1,53 @@
+import { Hub, Scope, getCurrentScope, makeMain, withScope } from '../../src';
+import { TestClient, getDefaultTestClientOptions } from '../mocks/client';
+
+function getTestClient(): TestClient {
+ return new TestClient(
+ getDefaultTestClientOptions({
+ dsn: 'https://username@domain/123',
+ }),
+ );
+}
+
+describe('withScope', () => {
+ beforeEach(() => {
+ const client = getTestClient();
+ const hub = new Hub(client);
+ makeMain(hub);
+ });
+
+ it('works without a return value', () => {
+ const scope1 = getCurrentScope();
+ expect(scope1).toBeInstanceOf(Scope);
+
+ scope1.setTag('foo', 'bar');
+
+ const res = withScope(scope => {
+ expect(scope).toBeInstanceOf(Scope);
+ expect(scope).not.toBe(scope1);
+ expect(scope['_tags']).toEqual({ foo: 'bar' });
+
+ expect(getCurrentScope()).toBe(scope);
+ });
+
+ expect(getCurrentScope()).toBe(scope1);
+ expect(res).toBe(undefined);
+ });
+
+ it('works with a return value', () => {
+ const res = withScope(scope => {
+ return 'foo';
+ });
+
+ expect(res).toBe('foo');
+ });
+
+ it('works with an async function', async () => {
+ const res = withScope(async scope => {
+ return 'foo';
+ });
+
+ expect(res).toBeInstanceOf(Promise);
+ expect(await res).toBe('foo');
+ });
+});
diff --git a/packages/core/test/lib/hint.test.ts b/packages/core/test/lib/hint.test.ts
index cdcfa9368cbe..5fb69ce39fff 100644
--- a/packages/core/test/lib/hint.test.ts
+++ b/packages/core/test/lib/hint.test.ts
@@ -1,4 +1,4 @@
-import { captureEvent, configureScope } from '@sentry/core';
+import { captureEvent, getCurrentScope } from '@sentry/core';
import { GLOBAL_OBJ } from '@sentry/utils';
import { initAndBind } from '../../src/sdk';
@@ -109,7 +109,7 @@ describe('Hint', () => {
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
initAndBind(TestClient, options);
- configureScope(scope => scope.addAttachment({ filename: 'scope.file', data: 'great content!' }));
+ getCurrentScope().addAttachment({ filename: 'scope.file', data: 'great content!' });
captureEvent({}, { attachments: [{ filename: 'some-file.txt', data: 'Hello' }] });
diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts
index 54bd426abb5c..65bf30483d86 100644
--- a/packages/core/test/lib/integration.test.ts
+++ b/packages/core/test/lib/integration.test.ts
@@ -2,7 +2,13 @@ import type { Integration, Options } from '@sentry/types';
import { logger } from '@sentry/utils';
import { Hub, makeMain } from '../../src/hub';
-import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration';
+import {
+ addIntegration,
+ convertIntegrationFnToClass,
+ getIntegrationsToSetup,
+ installedIntegrations,
+ setupIntegration,
+} from '../../src/integration';
import { TestClient, getDefaultTestClientOptions } from '../mocks/client';
function getTestClient(): TestClient {
@@ -647,3 +653,51 @@ describe('addIntegration', () => {
expect(warnings).toHaveBeenCalledWith('Cannot add integration "test" because no SDK Client is available.');
});
});
+
+describe('convertIntegrationFnToClass', () => {
+ /* eslint-disable deprecation/deprecation */
+ it('works with a minimal integration', () => {
+ const integrationFn = () => ({ name: 'testName' });
+
+ const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn);
+
+ expect(IntegrationClass.id).toBe('testName');
+
+ const integration = new IntegrationClass();
+ expect(integration).toEqual({
+ name: 'testName',
+ setupOnce: expect.any(Function),
+ });
+ });
+
+ it('works with integration hooks', () => {
+ const setup = jest.fn();
+ const setupOnce = jest.fn();
+ const processEvent = jest.fn();
+ const preprocessEvent = jest.fn();
+
+ const integrationFn = () => {
+ return {
+ name: 'testName',
+ setup,
+ setupOnce,
+ processEvent,
+ preprocessEvent,
+ };
+ };
+
+ const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn);
+
+ expect(IntegrationClass.id).toBe('testName');
+
+ const integration = new IntegrationClass();
+ expect(integration).toEqual({
+ name: 'testName',
+ setupOnce,
+ setup,
+ processEvent,
+ preprocessEvent,
+ });
+ });
+ /* eslint-enable deprecation/deprecation */
+});
diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts
index f4de76234ca2..20db043865a9 100644
--- a/packages/core/test/lib/tracing/errors.test.ts
+++ b/packages/core/test/lib/tracing/errors.test.ts
@@ -40,7 +40,7 @@ describe('registerErrorHandlers()', () => {
});
afterEach(() => {
- hub.configureScope(scope => scope.setSpan(undefined));
+ hub.getScope().setSpan(undefined);
});
it('registers error instrumentation', () => {
@@ -67,7 +67,7 @@ describe('registerErrorHandlers()', () => {
it('sets status for transaction on scope on error', () => {
registerErrorInstrumentation();
const transaction = hub.startTransaction({ name: 'test' });
- hub.configureScope(scope => scope.setSpan(transaction));
+ hub.getScope().setSpan(transaction);
mockErrorCallback({} as HandlerDataError);
expect(transaction.status).toBe('internal_error');
@@ -78,7 +78,7 @@ describe('registerErrorHandlers()', () => {
it('sets status for transaction on scope on unhandledrejection', () => {
registerErrorInstrumentation();
const transaction = hub.startTransaction({ name: 'test' });
- hub.configureScope(scope => scope.setSpan(transaction));
+ hub.getScope().setSpan(transaction);
mockUnhandledRejectionCallback({});
expect(transaction.status).toBe('internal_error');
diff --git a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts
index b1671b9410e8..98fd7e54207b 100644
--- a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts
+++ b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts
@@ -1,4 +1,4 @@
-import type { Hub } from '@sentry/types';
+import type { Client, Hub } from '@sentry/types';
import { isSentryRequestUrl } from '../../../src';
@@ -12,15 +12,21 @@ describe('isSentryRequestUrl', () => {
['http://tunnel:4200/', 'sentry-dsn.com', 'http://tunnel:4200', true],
['http://tunnel:4200/a', 'sentry-dsn.com', 'http://tunnel:4200', false],
])('works with url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string, expected: boolean) => {
+ const client = {
+ getOptions: () => ({ tunnel }),
+ getDsn: () => ({ host: dsn }),
+ } as unknown as Client;
+
const hub = {
getClient: () => {
- return {
- getOptions: () => ({ tunnel }),
- getDsn: () => ({ host: dsn }),
- };
+ return client;
},
} as unknown as Hub;
+ // Works with hub passed
expect(isSentryRequestUrl(url, hub)).toBe(expected);
+
+ // Works with client passed
+ expect(isSentryRequestUrl(url, client)).toBe(expected);
});
});
diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts
index ce95d04520a7..4c229ce27294 100644
--- a/packages/core/test/mocks/integration.ts
+++ b/packages/core/test/mocks/integration.ts
@@ -1,6 +1,6 @@
import type { Event, EventProcessor, Integration } from '@sentry/types';
-import { configureScope, getCurrentHub } from '../../src';
+import { getCurrentHub, getCurrentScope } from '../../src';
export class TestIntegration implements Integration {
public static id: string = 'TestIntegration';
@@ -18,9 +18,7 @@ export class TestIntegration implements Integration {
eventProcessor.id = this.name;
- configureScope(scope => {
- scope.addEventProcessor(eventProcessor);
- });
+ getCurrentScope().addEventProcessor(eventProcessor);
}
}
diff --git a/packages/deno/lib.deno.d.ts b/packages/deno/lib.deno.d.ts
index b576de192b36..62eec898407c 100644
--- a/packages/deno/lib.deno.d.ts
+++ b/packages/deno/lib.deno.d.ts
@@ -544,7 +544,7 @@ declare namespace Deno {
* Examples:
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test({
* name: "inherit",
@@ -559,7 +559,7 @@ declare namespace Deno {
* ```
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test({
* name: "true",
@@ -574,7 +574,7 @@ declare namespace Deno {
* ```
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test({
* name: "false",
@@ -589,7 +589,7 @@ declare namespace Deno {
* ```
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test({
* name: "localhost:8080",
@@ -818,7 +818,7 @@ declare namespace Deno {
* `fn` can be async if required.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test({
* name: "example test",
@@ -859,7 +859,7 @@ declare namespace Deno {
* `fn` can be async if required.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test({
* name: "example test",
@@ -896,7 +896,7 @@ declare namespace Deno {
* `fn` can be async if required.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test("My test description", () => {
* assertEquals("hello", "hello");
@@ -922,7 +922,7 @@ declare namespace Deno {
* `fn` can be async if required. Declared function must have a name.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test(function myTestName() {
* assertEquals("hello", "hello");
@@ -945,7 +945,7 @@ declare namespace Deno {
* `fn` can be async if required.
*
* ```ts
- * import {assert, fail, assertEquals} from "https://deno.land/std/testing/asserts.ts";
+ * import {assert, fail, assertEquals} from "https://deno.land/std/assert/mod.ts";
*
* Deno.test("My test description", { permissions: { read: true } }, (): void => {
* assertEquals("hello", "hello");
@@ -972,7 +972,7 @@ declare namespace Deno {
* `fn` can be async if required.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test(
* {
@@ -1010,7 +1010,7 @@ declare namespace Deno {
* `fn` can be async if required. Declared function must have a name.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.test(
* { permissions: { read: true } },
@@ -1234,7 +1234,7 @@ declare namespace Deno {
* will await resolution to consider the test complete.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.bench({
* name: "example test",
@@ -1273,7 +1273,7 @@ declare namespace Deno {
* will await resolution to consider the test complete.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.bench("My test description", () => {
* assertEquals("hello", "hello");
@@ -1301,7 +1301,7 @@ declare namespace Deno {
* will await resolution to consider the test complete.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.bench(function myTestName() {
* assertEquals("hello", "hello");
@@ -1326,7 +1326,7 @@ declare namespace Deno {
* will await resolution to consider the test complete.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.bench(
* "My test description",
@@ -1363,7 +1363,7 @@ declare namespace Deno {
* will await resolution to consider the test complete.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.bench(
* { name: "My test description", permissions: { read: true } },
@@ -1397,7 +1397,7 @@ declare namespace Deno {
* will await resolution to consider the test complete.
*
* ```ts
- * import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
+ * import { assertEquals } from "https://deno.land/std/assert/mod.ts";
*
* Deno.bench(
* { permissions: { read: true } },
@@ -1624,6 +1624,9 @@ declare namespace Deno {
* An abstract interface which when implemented provides an interface to read
* bytes into an array buffer asynchronously.
*
+ * @deprecated Use {@linkcode ReadableStream} instead. {@linkcode Reader}
+ * will be removed in v2.0.0.
+ *
* @category I/O */
export interface Reader {
/** Reads up to `p.byteLength` bytes into `p`. It resolves to the number of
@@ -1658,6 +1661,9 @@ declare namespace Deno {
* An abstract interface which when implemented provides an interface to read
* bytes into an array buffer synchronously.
*
+ * @deprecated Use {@linkcode ReadableStream} instead. {@linkcode ReaderSync}
+ * will be removed in v2.0.0.
+ *
* @category I/O */
export interface ReaderSync {
/** Reads up to `p.byteLength` bytes into `p`. It resolves to the number
@@ -1692,6 +1698,9 @@ declare namespace Deno {
* An abstract interface which when implemented provides an interface to write
* bytes from an array buffer to a file/resource asynchronously.
*
+ * @deprecated Use {@linkcode WritableStream} instead. {@linkcode Writer}
+ * will be removed in v2.0.0.
+ *
* @category I/O */
export interface Writer {
/** Writes `p.byteLength` bytes from `p` to the underlying data stream. It
@@ -1716,6 +1725,9 @@ declare namespace Deno {
* An abstract interface which when implemented provides an interface to write
* bytes from an array buffer to a file/resource synchronously.
*
+ * @deprecated Use {@linkcode WritableStream} instead. {@linkcode WriterSync}
+ * will be removed in v2.0.0.
+ *
* @category I/O */
export interface WriterSync {
/** Writes `p.byteLength` bytes from `p` to the underlying data
@@ -1734,6 +1746,9 @@ declare namespace Deno {
* An abstract interface which when implemented provides an interface to close
* files/resources that were previously opened.
*
+ * @deprecated Use {@linkcode ReadableStream} and {@linkcode WritableStream}
+ * instead. {@linkcode Closer} will be removed in v2.0.0.
+ *
* @category I/O */
export interface Closer {
/** Closes the resource, "freeing" the backing file/resource. */
@@ -2451,7 +2466,7 @@ declare namespace Deno {
/** Resolves to a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const file = await Deno.open("hello.txt");
* const fileInfo = await file.stat();
@@ -2463,7 +2478,7 @@ declare namespace Deno {
/** Synchronously returns a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const file = Deno.openSync("hello.txt")
* const fileInfo = file.statSync();
@@ -3289,10 +3304,10 @@ declare namespace Deno {
*
* _Linux/Mac OS only._ */
ino: number | null;
- /** **UNSTABLE**: Match behavior with Go on Windows for `mode`.
+ /** The underlying raw `st_mode` bits that contain the standard Unix
+ * permissions for this file/directory.
*
- * The underlying raw `st_mode` bits that contain the standard Unix
- * permissions for this file/directory. */
+ * _Linux/Mac OS only._ */
mode: number | null;
/** Number of hard links pointing to this file.
*
@@ -3513,7 +3528,7 @@ declare namespace Deno {
* of what it points to.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
* const fileInfo = await Deno.lstat("hello.txt");
* assert(fileInfo.isFile);
* ```
@@ -3530,7 +3545,7 @@ declare namespace Deno {
* returned instead of what it points to.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
* const fileInfo = Deno.lstatSync("hello.txt");
* assert(fileInfo.isFile);
* ```
@@ -3546,7 +3561,7 @@ declare namespace Deno {
* always follow symlinks.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
* const fileInfo = await Deno.stat("hello.txt");
* assert(fileInfo.isFile);
* ```
@@ -3562,7 +3577,7 @@ declare namespace Deno {
* `path`. Will always follow symlinks.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
* const fileInfo = Deno.statSync("hello.txt");
* assert(fileInfo.isFile);
* ```
@@ -4312,7 +4327,7 @@ declare namespace Deno {
*
* @category Sub Process
*/
- export class ChildProcess implements Disposable {
+ export class ChildProcess implements AsyncDisposable {
get stdin(): WritableStream;
get stdout(): ReadableStream;
get stderr(): ReadableStream;
@@ -4338,7 +4353,7 @@ declare namespace Deno {
* process from exiting. */
unref(): void;
- [Symbol.dispose](): void;
+ [Symbol.asyncDispose](): Promise;
}
/**
@@ -4794,7 +4809,7 @@ declare namespace Deno {
/** Revokes a permission, and resolves to the state of the permission.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const status = await Deno.permissions.revoke({ name: "run" });
* assert(status.state !== "granted")
@@ -4805,7 +4820,7 @@ declare namespace Deno {
/** Revokes a permission, and returns the state of the permission.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const status = Deno.permissions.revokeSync({ name: "run" });
* assert(status.state !== "granted")
@@ -4883,14 +4898,14 @@ declare namespace Deno {
* ### Revoking
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const status = await Deno.permissions.revoke({ name: "run" });
* assert(status.state !== "granted")
* ```
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const status = Deno.permissions.revokeSync({ name: "run" });
* assert(status.state !== "granted")
@@ -4986,13 +5001,13 @@ declare namespace Deno {
* Give the following command line invocation of Deno:
*
* ```sh
- * deno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd
+ * deno run --allow-read https://examples.deno.land/command-line-arguments.ts Sushi
* ```
*
* Then `Deno.args` will contain:
*
* ```ts
- * [ "/etc/passwd" ]
+ * [ "Sushi" ]
* ```
*
* If you are looking for a structured way to parse arguments, there is the
@@ -5200,7 +5215,7 @@ declare namespace Deno {
* Returns a `Deno.FileInfo` for the given file stream.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const file = await Deno.open("file.txt", { read: true });
* const fileInfo = await Deno.fstat(file.rid);
@@ -5216,7 +5231,7 @@ declare namespace Deno {
* stream.
*
* ```ts
- * import { assert } from "https://deno.land/std/testing/asserts.ts";
+ * import { assert } from "https://deno.land/std/assert/mod.ts";
*
* const file = Deno.openSync("file.txt", { read: true });
* const fileInfo = Deno.fstatSync(file.rid);
@@ -5939,6 +5954,50 @@ declare namespace Deno {
handler: ServeHandler;
}
+ export interface ServeUnixOptions {
+ /** The unix domain socket path to listen on. */
+ path: string;
+
+ /** An {@linkcode AbortSignal} to close the server and all connections. */
+ signal?: AbortSignal;
+
+ /** The handler to invoke when route handlers throw an error. */
+ onError?: (error: unknown) => Response | Promise;
+
+ /** The callback which is called when the server starts listening. */
+ onListen?: (params: { path: string }) => void;
+ }
+
+ /** Information for a unix domain socket HTTP request.
+ *
+ * @category HTTP Server
+ */
+ export interface ServeUnixHandlerInfo {
+ /** The remote address of the connection. */
+ remoteAddr: Deno.UnixAddr;
+ }
+
+ /** A handler for unix domain socket HTTP requests. Consumes a request and returns a response.
+ *
+ * If a handler throws, the server calling the handler will assume the impact
+ * of the error is isolated to the individual request. It will catch the error
+ * and if necessary will close the underlying connection.
+ *
+ * @category HTTP Server
+ */
+ export type ServeUnixHandler = (
+ request: Request,
+ info: ServeUnixHandlerInfo,
+ ) => Response | Promise;
+
+ /**
+ * @category HTTP Server
+ */
+ export interface ServeUnixInit {
+ /** The handler to invoke to process each incoming request. */
+ handler: ServeUnixHandler;
+ }
+
/** An instance of the server created using `Deno.serve()` API.
*
* @category HTTP Server
@@ -5959,6 +6018,11 @@ declare namespace Deno {
/** Make the server not block the event loop from finishing. */
unref(): void;
+
+ /** Gracefully close the server. No more new connections will be accepted,
+ * while pending requests will be allowed to finish.
+ */
+ shutdown(): Promise;
}
/**
@@ -5978,6 +6042,55 @@ declare namespace Deno {
* @category HTTP Server
*/
export function serve(handler: ServeHandler): HttpServer;
+ /** Serves HTTP requests with the given option bag and handler.
+ *
+ * You can specify the socket path with `path` option.
+ *
+ * ```ts
+ * Deno.serve(
+ * { path: "path/to/socket" },
+ * (_req) => new Response("Hello, world")
+ * );
+ * ```
+ *
+ * You can stop the server with an {@linkcode AbortSignal}. The abort signal
+ * needs to be passed as the `signal` option in the options bag. The server
+ * aborts when the abort signal is aborted. To wait for the server to close,
+ * await the promise returned from the `Deno.serve` API.
+ *
+ * ```ts
+ * const ac = new AbortController();
+ *
+ * const server = Deno.serve(
+ * { signal: ac.signal, path: "path/to/socket" },
+ * (_req) => new Response("Hello, world")
+ * );
+ * server.finished.then(() => console.log("Server closed"));
+ *
+ * console.log("Closing server...");
+ * ac.abort();
+ * ```
+ *
+ * By default `Deno.serve` prints the message
+ * `Listening on path/to/socket` on listening. If you like to
+ * change this behavior, you can specify a custom `onListen` callback.
+ *
+ * ```ts
+ * Deno.serve({
+ * onListen({ path }) {
+ * console.log(`Server started at ${path}`);
+ * // ... more info specific to your server ..
+ * },
+ * path: "path/to/socket",
+ * }, (_req) => new Response("Hello, world"));
+ * ```
+ *
+ * @category HTTP Server
+ */
+ export function serve(
+ options: ServeUnixOptions,
+ handler: ServeUnixHandler,
+ ): HttpServer;
/** Serves HTTP requests with the given option bag and handler.
*
* You can specify an object with a port and hostname option, which is the
@@ -6038,6 +6151,33 @@ declare namespace Deno {
options: ServeOptions | ServeTlsOptions,
handler: ServeHandler,
): HttpServer;
+ /** Serves HTTP requests with the given option bag.
+ *
+ * You can specify an object with the path option, which is the
+ * unix domain socket to listen on.
+ *
+ * ```ts
+ * const ac = new AbortController();
+ *
+ * const server = Deno.serve({
+ * path: "path/to/socket",
+ * handler: (_req) => new Response("Hello, world"),
+ * signal: ac.signal,
+ * onListen({ path }) {
+ * console.log(`Server started at ${path}`);
+ * },
+ * });
+ * server.finished.then(() => console.log("Server closed"));
+ *
+ * console.log("Closing server...");
+ * ac.abort();
+ * ```
+ *
+ * @category HTTP Server
+ */
+ export function serve(
+ options: ServeUnixInit & ServeUnixOptions,
+ ): HttpServer;
/** Serves HTTP requests with the given option bag.
*
* You can specify an object with a port and hostname option, which is the
@@ -7113,12 +7253,18 @@ declare type ReadableStreamBYOBReadResult =
| ReadableStreamBYOBReadDoneResult
| ReadableStreamBYOBReadValueResult;
+/** @category Streams API */
+declare interface ReadableStreamBYOBReaderReadOptions {
+ min?: number;
+}
+
/** @category Streams API */
declare interface ReadableStreamBYOBReader {
readonly closed: Promise;
cancel(reason?: any): Promise;
read(
view: V,
+ options?: ReadableStreamBYOBReaderReadOptions,
): Promise>;
releaseLock(): void;
}
@@ -7722,6 +7868,34 @@ declare function reportError(
error: any,
): void;
+/** @category Web APIs */
+type PredefinedColorSpace = "srgb" | "display-p3";
+
+/** @category Web APIs */
+interface ImageDataSettings {
+ readonly colorSpace?: PredefinedColorSpace;
+}
+
+/** @category Web APIs */
+interface ImageData {
+ readonly colorSpace: PredefinedColorSpace;
+ readonly data: Uint8ClampedArray;
+ readonly height: number;
+ readonly width: number;
+}
+
+/** @category Web APIs */
+declare var ImageData: {
+ prototype: ImageData;
+ new (sw: number, sh: number, settings?: ImageDataSettings): ImageData;
+ new (
+ data: Uint8ClampedArray,
+ sw: number,
+ sh?: number,
+ settings?: ImageDataSettings,
+ ): ImageData;
+};
+
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file no-explicit-any no-var
@@ -8132,6 +8306,1322 @@ declare function fetch(
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// deno-lint-ignore-file no-explicit-any no-empty-interface
+
+///
+///
+
+/** @category WebGPU */
+interface GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUObjectDescriptorBase {
+ label?: string;
+}
+
+/** @category WebGPU */
+declare class GPUSupportedLimits {
+ maxTextureDimension1D?: number;
+ maxTextureDimension2D?: number;
+ maxTextureDimension3D?: number;
+ maxTextureArrayLayers?: number;
+ maxBindGroups?: number;
+ maxBindingsPerBindGroup?: number;
+ maxDynamicUniformBuffersPerPipelineLayout?: number;
+ maxDynamicStorageBuffersPerPipelineLayout?: number;
+ maxSampledTexturesPerShaderStage?: number;
+ maxSamplersPerShaderStage?: number;
+ maxStorageBuffersPerShaderStage?: number;
+ maxStorageTexturesPerShaderStage?: number;
+ maxUniformBuffersPerShaderStage?: number;
+ maxUniformBufferBindingSize?: number;
+ maxStorageBufferBindingSize?: number;
+ minUniformBufferOffsetAlignment?: number;
+ minStorageBufferOffsetAlignment?: number;
+ maxVertexBuffers?: number;
+ maxBufferSize?: number;
+ maxVertexAttributes?: number;
+ maxVertexBufferArrayStride?: number;
+ maxInterStageShaderComponents?: number;
+ maxComputeWorkgroupStorageSize?: number;
+ maxComputeInvocationsPerWorkgroup?: number;
+ maxComputeWorkgroupSizeX?: number;
+ maxComputeWorkgroupSizeY?: number;
+ maxComputeWorkgroupSizeZ?: number;
+ maxComputeWorkgroupsPerDimension?: number;
+}
+
+/** @category WebGPU */
+declare class GPUSupportedFeatures {
+ forEach(
+ callbackfn: (
+ value: GPUFeatureName,
+ value2: GPUFeatureName,
+ set: Set,
+ ) => void,
+ thisArg?: any,
+ ): void;
+ has(value: GPUFeatureName): boolean;
+ size: number;
+ [Symbol.iterator](): IterableIterator;
+ entries(): IterableIterator<[GPUFeatureName, GPUFeatureName]>;
+ keys(): IterableIterator;
+ values(): IterableIterator;
+}
+
+/** @category WebGPU */
+declare class GPUAdapterInfo {
+ readonly vendor: string;
+ readonly architecture: string;
+ readonly device: string;
+ readonly description: string;
+}
+
+/** @category WebGPU */
+declare class GPU {
+ requestAdapter(
+ options?: GPURequestAdapterOptions,
+ ): Promise;
+}
+
+/** @category WebGPU */
+declare interface GPURequestAdapterOptions {
+ powerPreference?: GPUPowerPreference;
+ forceFallbackAdapter?: boolean;
+}
+
+/** @category WebGPU */
+declare type GPUPowerPreference = "low-power" | "high-performance";
+
+/** @category WebGPU */
+declare class GPUAdapter {
+ readonly features: GPUSupportedFeatures;
+ readonly limits: GPUSupportedLimits;
+ readonly isFallbackAdapter: boolean;
+
+ requestDevice(descriptor?: GPUDeviceDescriptor): Promise;
+ requestAdapterInfo(unmaskHints?: string[]): Promise;
+}
+
+/** @category WebGPU */
+declare interface GPUDeviceDescriptor extends GPUObjectDescriptorBase {
+ requiredFeatures?: GPUFeatureName[];
+ requiredLimits?: Record;
+}
+
+/** @category WebGPU */
+declare type GPUFeatureName =
+ | "depth-clip-control"
+ | "depth32float-stencil8"
+ | "pipeline-statistics-query"
+ | "texture-compression-bc"
+ | "texture-compression-etc2"
+ | "texture-compression-astc"
+ | "timestamp-query"
+ | "indirect-first-instance"
+ | "shader-f16"
+ // extended from spec
+ | "mappable-primary-buffers"
+ | "sampled-texture-binding-array"
+ | "sampled-texture-array-dynamic-indexing"
+ | "sampled-texture-array-non-uniform-indexing"
+ | "unsized-binding-array"
+ | "multi-draw-indirect"
+ | "multi-draw-indirect-count"
+ | "push-constants"
+ | "address-mode-clamp-to-border"
+ | "texture-adapter-specific-format-features"
+ | "shader-float64"
+ | "vertex-attribute-64bit";
+
+/** @category WebGPU */
+declare class GPUDevice extends EventTarget implements GPUObjectBase {
+ label: string;
+
+ readonly lost: Promise;
+ pushErrorScope(filter: GPUErrorFilter): undefined;
+ popErrorScope(): Promise;
+
+ readonly features: GPUSupportedFeatures;
+ readonly limits: GPUSupportedLimits;
+ readonly queue: GPUQueue;
+
+ destroy(): undefined;
+
+ createBuffer(descriptor: GPUBufferDescriptor): GPUBuffer;
+ createTexture(descriptor: GPUTextureDescriptor): GPUTexture;
+ createSampler(descriptor?: GPUSamplerDescriptor): GPUSampler;
+
+ createBindGroupLayout(
+ descriptor: GPUBindGroupLayoutDescriptor,
+ ): GPUBindGroupLayout;
+ createPipelineLayout(
+ descriptor: GPUPipelineLayoutDescriptor,
+ ): GPUPipelineLayout;
+ createBindGroup(descriptor: GPUBindGroupDescriptor): GPUBindGroup;
+
+ createShaderModule(descriptor: GPUShaderModuleDescriptor): GPUShaderModule;
+ createComputePipeline(
+ descriptor: GPUComputePipelineDescriptor,
+ ): GPUComputePipeline;
+ createRenderPipeline(
+ descriptor: GPURenderPipelineDescriptor,
+ ): GPURenderPipeline;
+ createComputePipelineAsync(
+ descriptor: GPUComputePipelineDescriptor,
+ ): Promise;
+ createRenderPipelineAsync(
+ descriptor: GPURenderPipelineDescriptor,
+ ): Promise;
+
+ createCommandEncoder(
+ descriptor?: GPUCommandEncoderDescriptor,
+ ): GPUCommandEncoder;
+ createRenderBundleEncoder(
+ descriptor: GPURenderBundleEncoderDescriptor,
+ ): GPURenderBundleEncoder;
+
+ createQuerySet(descriptor: GPUQuerySetDescriptor): GPUQuerySet;
+}
+
+/** @category WebGPU */
+declare class GPUBuffer implements GPUObjectBase {
+ label: string;
+
+ readonly size: number;
+ readonly usage: GPUFlagsConstant;
+ readonly mapState: GPUBufferMapState;
+
+ mapAsync(
+ mode: GPUMapModeFlags,
+ offset?: number,
+ size?: number,
+ ): Promise;
+ getMappedRange(offset?: number, size?: number): ArrayBuffer;
+ unmap(): undefined;
+
+ destroy(): undefined;
+}
+
+/** @category WebGPU */
+declare type GPUBufferMapState = "unmapped" | "pending" | "mapped";
+
+/** @category WebGPU */
+declare interface GPUBufferDescriptor extends GPUObjectDescriptorBase {
+ size: number;
+ usage: GPUBufferUsageFlags;
+ mappedAtCreation?: boolean;
+}
+
+/** @category WebGPU */
+declare type GPUBufferUsageFlags = number;
+
+/** @category WebGPU */
+declare type GPUFlagsConstant = number;
+
+/** @category WebGPU */
+declare class GPUBufferUsage {
+ static MAP_READ: 0x0001;
+ static MAP_WRITE: 0x0002;
+ static COPY_SRC: 0x0004;
+ static COPY_DST: 0x0008;
+ static INDEX: 0x0010;
+ static VERTEX: 0x0020;
+ static UNIFORM: 0x0040;
+ static STORAGE: 0x0080;
+ static INDIRECT: 0x0100;
+ static QUERY_RESOLVE: 0x0200;
+}
+
+/** @category WebGPU */
+declare type GPUMapModeFlags = number;
+
+/** @category WebGPU */
+declare class GPUMapMode {
+ static READ: 0x0001;
+ static WRITE: 0x0002;
+}
+
+/** @category WebGPU */
+declare class GPUTexture implements GPUObjectBase {
+ label: string;
+
+ createView(descriptor?: GPUTextureViewDescriptor): GPUTextureView;
+ destroy(): undefined;
+
+ readonly width: number;
+ readonly height: number;
+ readonly depthOrArrayLayers: number;
+ readonly mipLevelCount: number;
+ readonly sampleCount: number;
+ readonly dimension: GPUTextureDimension;
+ readonly format: GPUTextureFormat;
+ readonly usage: GPUFlagsConstant;
+}
+
+/** @category WebGPU */
+declare interface GPUTextureDescriptor extends GPUObjectDescriptorBase {
+ size: GPUExtent3D;
+ mipLevelCount?: number;
+ sampleCount?: number;
+ dimension?: GPUTextureDimension;
+ format: GPUTextureFormat;
+ usage: GPUTextureUsageFlags;
+ viewFormats?: GPUTextureFormat[];
+}
+
+/** @category WebGPU */
+declare type GPUTextureDimension = "1d" | "2d" | "3d";
+
+/** @category WebGPU */
+declare type GPUTextureUsageFlags = number;
+
+/** @category WebGPU */
+declare class GPUTextureUsage {
+ static COPY_SRC: 0x01;
+ static COPY_DST: 0x02;
+ static TEXTURE_BINDING: 0x04;
+ static STORAGE_BINDING: 0x08;
+ static RENDER_ATTACHMENT: 0x10;
+}
+
+/** @category WebGPU */
+declare class GPUTextureView implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUTextureViewDescriptor extends GPUObjectDescriptorBase {
+ format?: GPUTextureFormat;
+ dimension?: GPUTextureViewDimension;
+ aspect?: GPUTextureAspect;
+ baseMipLevel?: number;
+ mipLevelCount?: number;
+ baseArrayLayer?: number;
+ arrayLayerCount?: number;
+}
+
+/** @category WebGPU */
+declare type GPUTextureViewDimension =
+ | "1d"
+ | "2d"
+ | "2d-array"
+ | "cube"
+ | "cube-array"
+ | "3d";
+
+/** @category WebGPU */
+declare type GPUTextureAspect = "all" | "stencil-only" | "depth-only";
+
+/** @category WebGPU */
+declare type GPUTextureFormat =
+ | "r8unorm"
+ | "r8snorm"
+ | "r8uint"
+ | "r8sint"
+ | "r16uint"
+ | "r16sint"
+ | "r16float"
+ | "rg8unorm"
+ | "rg8snorm"
+ | "rg8uint"
+ | "rg8sint"
+ | "r32uint"
+ | "r32sint"
+ | "r32float"
+ | "rg16uint"
+ | "rg16sint"
+ | "rg16float"
+ | "rgba8unorm"
+ | "rgba8unorm-srgb"
+ | "rgba8snorm"
+ | "rgba8uint"
+ | "rgba8sint"
+ | "bgra8unorm"
+ | "bgra8unorm-srgb"
+ | "rgb9e5ufloat"
+ | "rgb10a2unorm"
+ | "rg11b10ufloat"
+ | "rg32uint"
+ | "rg32sint"
+ | "rg32float"
+ | "rgba16uint"
+ | "rgba16sint"
+ | "rgba16float"
+ | "rgba32uint"
+ | "rgba32sint"
+ | "rgba32float"
+ | "stencil8"
+ | "depth16unorm"
+ | "depth24plus"
+ | "depth24plus-stencil8"
+ | "depth32float"
+ | "depth32float-stencil8"
+ | "bc1-rgba-unorm"
+ | "bc1-rgba-unorm-srgb"
+ | "bc2-rgba-unorm"
+ | "bc2-rgba-unorm-srgb"
+ | "bc3-rgba-unorm"
+ | "bc3-rgba-unorm-srgb"
+ | "bc4-r-unorm"
+ | "bc4-r-snorm"
+ | "bc5-rg-unorm"
+ | "bc5-rg-snorm"
+ | "bc6h-rgb-ufloat"
+ | "bc6h-rgb-float"
+ | "bc7-rgba-unorm"
+ | "bc7-rgba-unorm-srgb"
+ | "etc2-rgb8unorm"
+ | "etc2-rgb8unorm-srgb"
+ | "etc2-rgb8a1unorm"
+ | "etc2-rgb8a1unorm-srgb"
+ | "etc2-rgba8unorm"
+ | "etc2-rgba8unorm-srgb"
+ | "eac-r11unorm"
+ | "eac-r11snorm"
+ | "eac-rg11unorm"
+ | "eac-rg11snorm"
+ | "astc-4x4-unorm"
+ | "astc-4x4-unorm-srgb"
+ | "astc-5x4-unorm"
+ | "astc-5x4-unorm-srgb"
+ | "astc-5x5-unorm"
+ | "astc-5x5-unorm-srgb"
+ | "astc-6x5-unorm"
+ | "astc-6x5-unorm-srgb"
+ | "astc-6x6-unorm"
+ | "astc-6x6-unorm-srgb"
+ | "astc-8x5-unorm"
+ | "astc-8x5-unorm-srgb"
+ | "astc-8x6-unorm"
+ | "astc-8x6-unorm-srgb"
+ | "astc-8x8-unorm"
+ | "astc-8x8-unorm-srgb"
+ | "astc-10x5-unorm"
+ | "astc-10x5-unorm-srgb"
+ | "astc-10x6-unorm"
+ | "astc-10x6-unorm-srgb"
+ | "astc-10x8-unorm"
+ | "astc-10x8-unorm-srgb"
+ | "astc-10x10-unorm"
+ | "astc-10x10-unorm-srgb"
+ | "astc-12x10-unorm"
+ | "astc-12x10-unorm-srgb"
+ | "astc-12x12-unorm"
+ | "astc-12x12-unorm-srgb";
+
+/** @category WebGPU */
+declare class GPUSampler implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUSamplerDescriptor extends GPUObjectDescriptorBase {
+ addressModeU?: GPUAddressMode;
+ addressModeV?: GPUAddressMode;
+ addressModeW?: GPUAddressMode;
+ magFilter?: GPUFilterMode;
+ minFilter?: GPUFilterMode;
+ mipmapFilter?: GPUMipmapFilterMode;
+ lodMinClamp?: number;
+ lodMaxClamp?: number;
+ compare?: GPUCompareFunction;
+ maxAnisotropy?: number;
+}
+
+/** @category WebGPU */
+declare type GPUAddressMode = "clamp-to-edge" | "repeat" | "mirror-repeat";
+
+/** @category WebGPU */
+declare type GPUFilterMode = "nearest" | "linear";
+
+/** @category WebGPU */
+declare type GPUMipmapFilterMode = "nearest" | "linear";
+
+/** @category WebGPU */
+declare type GPUCompareFunction =
+ | "never"
+ | "less"
+ | "equal"
+ | "less-equal"
+ | "greater"
+ | "not-equal"
+ | "greater-equal"
+ | "always";
+
+/** @category WebGPU */
+declare class GPUBindGroupLayout implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUBindGroupLayoutDescriptor extends GPUObjectDescriptorBase {
+ entries: GPUBindGroupLayoutEntry[];
+}
+
+/** @category WebGPU */
+declare interface GPUBindGroupLayoutEntry {
+ binding: number;
+ visibility: GPUShaderStageFlags;
+
+ buffer?: GPUBufferBindingLayout;
+ sampler?: GPUSamplerBindingLayout;
+ texture?: GPUTextureBindingLayout;
+ storageTexture?: GPUStorageTextureBindingLayout;
+}
+
+/** @category WebGPU */
+declare type GPUShaderStageFlags = number;
+
+/** @category WebGPU */
+declare class GPUShaderStage {
+ static VERTEX: 0x1;
+ static FRAGMENT: 0x2;
+ static COMPUTE: 0x4;
+}
+
+/** @category WebGPU */
+declare interface GPUBufferBindingLayout {
+ type?: GPUBufferBindingType;
+ hasDynamicOffset?: boolean;
+ minBindingSize?: number;
+}
+
+/** @category WebGPU */
+declare type GPUBufferBindingType = "uniform" | "storage" | "read-only-storage";
+
+/** @category WebGPU */
+declare interface GPUSamplerBindingLayout {
+ type?: GPUSamplerBindingType;
+}
+
+/** @category WebGPU */
+declare type GPUSamplerBindingType =
+ | "filtering"
+ | "non-filtering"
+ | "comparison";
+
+/** @category WebGPU */
+declare interface GPUTextureBindingLayout {
+ sampleType?: GPUTextureSampleType;
+ viewDimension?: GPUTextureViewDimension;
+ multisampled?: boolean;
+}
+
+/** @category WebGPU */
+declare type GPUTextureSampleType =
+ | "float"
+ | "unfilterable-float"
+ | "depth"
+ | "sint"
+ | "uint";
+
+/** @category WebGPU */
+declare type GPUStorageTextureAccess = "write-only";
+
+/** @category WebGPU */
+declare interface GPUStorageTextureBindingLayout {
+ access: GPUStorageTextureAccess;
+ format: GPUTextureFormat;
+ viewDimension?: GPUTextureViewDimension;
+}
+
+/** @category WebGPU */
+declare class GPUBindGroup implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUBindGroupDescriptor extends GPUObjectDescriptorBase {
+ layout: GPUBindGroupLayout;
+ entries: GPUBindGroupEntry[];
+}
+
+/** @category WebGPU */
+declare type GPUBindingResource =
+ | GPUSampler
+ | GPUTextureView
+ | GPUBufferBinding;
+
+/** @category WebGPU */
+declare interface GPUBindGroupEntry {
+ binding: number;
+ resource: GPUBindingResource;
+}
+
+/** @category WebGPU */
+declare interface GPUBufferBinding {
+ buffer: GPUBuffer;
+ offset?: number;
+ size?: number;
+}
+
+/** @category WebGPU */
+declare class GPUPipelineLayout implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUPipelineLayoutDescriptor extends GPUObjectDescriptorBase {
+ bindGroupLayouts: GPUBindGroupLayout[];
+}
+
+/** @category WebGPU */
+declare type GPUCompilationMessageType = "error" | "warning" | "info";
+
+/** @category WebGPU */
+declare interface GPUCompilationMessage {
+ readonly message: string;
+ readonly type: GPUCompilationMessageType;
+ readonly lineNum: number;
+ readonly linePos: number;
+}
+
+/** @category WebGPU */
+declare interface GPUCompilationInfo {
+ readonly messages: ReadonlyArray;
+}
+
+/** @category WebGPU */
+declare class GPUShaderModule implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUShaderModuleDescriptor extends GPUObjectDescriptorBase {
+ code: string;
+ sourceMap?: any;
+}
+
+/** @category WebGPU */
+declare type GPUAutoLayoutMode = "auto";
+
+/** @category WebGPU */
+declare interface GPUPipelineDescriptorBase extends GPUObjectDescriptorBase {
+ layout: GPUPipelineLayout | GPUAutoLayoutMode;
+}
+
+/** @category WebGPU */
+declare interface GPUPipelineBase {
+ getBindGroupLayout(index: number): GPUBindGroupLayout;
+}
+
+/** @category WebGPU */
+declare interface GPUProgrammableStage {
+ module: GPUShaderModule;
+ entryPoint: string;
+}
+
+/** @category WebGPU */
+declare class GPUComputePipeline implements GPUObjectBase, GPUPipelineBase {
+ label: string;
+
+ getBindGroupLayout(index: number): GPUBindGroupLayout;
+}
+
+/** @category WebGPU */
+declare interface GPUComputePipelineDescriptor
+ extends GPUPipelineDescriptorBase {
+ compute: GPUProgrammableStage;
+}
+
+/** @category WebGPU */
+declare class GPURenderPipeline implements GPUObjectBase, GPUPipelineBase {
+ label: string;
+
+ getBindGroupLayout(index: number): GPUBindGroupLayout;
+}
+
+/** @category WebGPU */
+declare interface GPURenderPipelineDescriptor
+ extends GPUPipelineDescriptorBase {
+ vertex: GPUVertexState;
+ primitive?: GPUPrimitiveState;
+ depthStencil?: GPUDepthStencilState;
+ multisample?: GPUMultisampleState;
+ fragment?: GPUFragmentState;
+}
+
+/** @category WebGPU */
+declare interface GPUPrimitiveState {
+ topology?: GPUPrimitiveTopology;
+ stripIndexFormat?: GPUIndexFormat;
+ frontFace?: GPUFrontFace;
+ cullMode?: GPUCullMode;
+ unclippedDepth?: boolean;
+}
+
+/** @category WebGPU */
+declare type GPUPrimitiveTopology =
+ | "point-list"
+ | "line-list"
+ | "line-strip"
+ | "triangle-list"
+ | "triangle-strip";
+
+/** @category WebGPU */
+declare type GPUFrontFace = "ccw" | "cw";
+
+/** @category WebGPU */
+declare type GPUCullMode = "none" | "front" | "back";
+
+/** @category WebGPU */
+declare interface GPUMultisampleState {
+ count?: number;
+ mask?: number;
+ alphaToCoverageEnabled?: boolean;
+}
+
+/** @category WebGPU */
+declare interface GPUFragmentState extends GPUProgrammableStage {
+ targets: (GPUColorTargetState | null)[];
+}
+
+/** @category WebGPU */
+declare interface GPUColorTargetState {
+ format: GPUTextureFormat;
+
+ blend?: GPUBlendState;
+ writeMask?: GPUColorWriteFlags;
+}
+
+/** @category WebGPU */
+declare interface GPUBlendState {
+ color: GPUBlendComponent;
+ alpha: GPUBlendComponent;
+}
+
+/** @category WebGPU */
+declare type GPUColorWriteFlags = number;
+
+/** @category WebGPU */
+declare class GPUColorWrite {
+ static RED: 0x1;
+ static GREEN: 0x2;
+ static BLUE: 0x4;
+ static ALPHA: 0x8;
+ static ALL: 0xF;
+}
+
+/** @category WebGPU */
+declare interface GPUBlendComponent {
+ operation?: GPUBlendOperation;
+ srcFactor?: GPUBlendFactor;
+ dstFactor?: GPUBlendFactor;
+}
+
+/** @category WebGPU */
+declare type GPUBlendFactor =
+ | "zero"
+ | "one"
+ | "src"
+ | "one-minus-src"
+ | "src-alpha"
+ | "one-minus-src-alpha"
+ | "dst"
+ | "one-minus-dst"
+ | "dst-alpha"
+ | "one-minus-dst-alpha"
+ | "src-alpha-saturated"
+ | "constant"
+ | "one-minus-constant";
+
+/** @category WebGPU */
+declare type GPUBlendOperation =
+ | "add"
+ | "subtract"
+ | "reverse-subtract"
+ | "min"
+ | "max";
+
+/** @category WebGPU */
+declare interface GPUDepthStencilState {
+ format: GPUTextureFormat;
+
+ depthWriteEnabled: boolean;
+ depthCompare: GPUCompareFunction;
+
+ stencilFront?: GPUStencilFaceState;
+ stencilBack?: GPUStencilFaceState;
+
+ stencilReadMask?: number;
+ stencilWriteMask?: number;
+
+ depthBias?: number;
+ depthBiasSlopeScale?: number;
+ depthBiasClamp?: number;
+}
+
+/** @category WebGPU */
+declare interface GPUStencilFaceState {
+ compare?: GPUCompareFunction;
+ failOp?: GPUStencilOperation;
+ depthFailOp?: GPUStencilOperation;
+ passOp?: GPUStencilOperation;
+}
+
+/** @category WebGPU */
+declare type GPUStencilOperation =
+ | "keep"
+ | "zero"
+ | "replace"
+ | "invert"
+ | "increment-clamp"
+ | "decrement-clamp"
+ | "increment-wrap"
+ | "decrement-wrap";
+
+/** @category WebGPU */
+declare type GPUIndexFormat = "uint16" | "uint32";
+
+/** @category WebGPU */
+declare type GPUVertexFormat =
+ | "uint8x2"
+ | "uint8x4"
+ | "sint8x2"
+ | "sint8x4"
+ | "unorm8x2"
+ | "unorm8x4"
+ | "snorm8x2"
+ | "snorm8x4"
+ | "uint16x2"
+ | "uint16x4"
+ | "sint16x2"
+ | "sint16x4"
+ | "unorm16x2"
+ | "unorm16x4"
+ | "snorm16x2"
+ | "snorm16x4"
+ | "float16x2"
+ | "float16x4"
+ | "float32"
+ | "float32x2"
+ | "float32x3"
+ | "float32x4"
+ | "uint32"
+ | "uint32x2"
+ | "uint32x3"
+ | "uint32x4"
+ | "sint32"
+ | "sint32x2"
+ | "sint32x3"
+ | "sint32x4";
+
+/** @category WebGPU */
+declare type GPUVertexStepMode = "vertex" | "instance";
+
+/** @category WebGPU */
+declare interface GPUVertexState extends GPUProgrammableStage {
+ buffers?: (GPUVertexBufferLayout | null)[];
+}
+
+/** @category WebGPU */
+declare interface GPUVertexBufferLayout {
+ arrayStride: number;
+ stepMode?: GPUVertexStepMode;
+ attributes: GPUVertexAttribute[];
+}
+
+/** @category WebGPU */
+declare interface GPUVertexAttribute {
+ format: GPUVertexFormat;
+ offset: number;
+
+ shaderLocation: number;
+}
+
+/** @category WebGPU */
+declare interface GPUImageDataLayout {
+ offset?: number;
+ bytesPerRow?: number;
+ rowsPerImage?: number;
+}
+
+/** @category WebGPU */
+declare class GPUCommandBuffer implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPUCommandBufferDescriptor extends GPUObjectDescriptorBase {}
+
+/** @category WebGPU */
+declare class GPUCommandEncoder implements GPUObjectBase {
+ label: string;
+
+ beginRenderPass(descriptor: GPURenderPassDescriptor): GPURenderPassEncoder;
+ beginComputePass(
+ descriptor?: GPUComputePassDescriptor,
+ ): GPUComputePassEncoder;
+
+ copyBufferToBuffer(
+ source: GPUBuffer,
+ sourceOffset: number,
+ destination: GPUBuffer,
+ destinationOffset: number,
+ size: number,
+ ): undefined;
+
+ copyBufferToTexture(
+ source: GPUImageCopyBuffer,
+ destination: GPUImageCopyTexture,
+ copySize: GPUExtent3D,
+ ): undefined;
+
+ copyTextureToBuffer(
+ source: GPUImageCopyTexture,
+ destination: GPUImageCopyBuffer,
+ copySize: GPUExtent3D,
+ ): undefined;
+
+ copyTextureToTexture(
+ source: GPUImageCopyTexture,
+ destination: GPUImageCopyTexture,
+ copySize: GPUExtent3D,
+ ): undefined;
+
+ clearBuffer(
+ destination: GPUBuffer,
+ destinationOffset?: number,
+ size?: number,
+ ): undefined;
+
+ pushDebugGroup(groupLabel: string): undefined;
+ popDebugGroup(): undefined;
+ insertDebugMarker(markerLabel: string): undefined;
+
+ writeTimestamp(querySet: GPUQuerySet, queryIndex: number): undefined;
+
+ resolveQuerySet(
+ querySet: GPUQuerySet,
+ firstQuery: number,
+ queryCount: number,
+ destination: GPUBuffer,
+ destinationOffset: number,
+ ): undefined;
+
+ finish(descriptor?: GPUCommandBufferDescriptor): GPUCommandBuffer;
+}
+
+/** @category WebGPU */
+declare interface GPUCommandEncoderDescriptor extends GPUObjectDescriptorBase {}
+
+/** @category WebGPU */
+declare interface GPUImageCopyBuffer extends GPUImageDataLayout {
+ buffer: GPUBuffer;
+}
+
+/** @category WebGPU */
+declare interface GPUImageCopyTexture {
+ texture: GPUTexture;
+ mipLevel?: number;
+ origin?: GPUOrigin3D;
+ aspect?: GPUTextureAspect;
+}
+
+/** @category WebGPU */
+interface GPUProgrammablePassEncoder {
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsets?: number[],
+ ): undefined;
+
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsetsData: Uint32Array,
+ dynamicOffsetsDataStart: number,
+ dynamicOffsetsDataLength: number,
+ ): undefined;
+
+ pushDebugGroup(groupLabel: string): undefined;
+ popDebugGroup(): undefined;
+ insertDebugMarker(markerLabel: string): undefined;
+}
+
+/** @category WebGPU */
+declare class GPUComputePassEncoder
+ implements GPUObjectBase, GPUProgrammablePassEncoder {
+ label: string;
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsets?: number[],
+ ): undefined;
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsetsData: Uint32Array,
+ dynamicOffsetsDataStart: number,
+ dynamicOffsetsDataLength: number,
+ ): undefined;
+ pushDebugGroup(groupLabel: string): undefined;
+ popDebugGroup(): undefined;
+ insertDebugMarker(markerLabel: string): undefined;
+ setPipeline(pipeline: GPUComputePipeline): undefined;
+ dispatchWorkgroups(x: number, y?: number, z?: number): undefined;
+ dispatchWorkgroupsIndirect(
+ indirectBuffer: GPUBuffer,
+ indirectOffset: number,
+ ): undefined;
+
+ end(): undefined;
+}
+
+/** @category WebGPU */
+declare interface GPUComputePassTimestampWrites {
+ querySet: GPUQuerySet;
+ beginningOfPassWriteIndex?: number;
+ endOfPassWriteIndex?: number;
+}
+
+/** @category WebGPU */
+declare interface GPUComputePassDescriptor extends GPUObjectDescriptorBase {
+ timestampWrites?: GPUComputePassTimestampWrites;
+}
+
+/** @category WebGPU */
+interface GPURenderEncoderBase {
+ setPipeline(pipeline: GPURenderPipeline): undefined;
+
+ setIndexBuffer(
+ buffer: GPUBuffer,
+ indexFormat: GPUIndexFormat,
+ offset?: number,
+ size?: number,
+ ): undefined;
+ setVertexBuffer(
+ slot: number,
+ buffer: GPUBuffer,
+ offset?: number,
+ size?: number,
+ ): undefined;
+
+ draw(
+ vertexCount: number,
+ instanceCount?: number,
+ firstVertex?: number,
+ firstInstance?: number,
+ ): undefined;
+ drawIndexed(
+ indexCount: number,
+ instanceCount?: number,
+ firstIndex?: number,
+ baseVertex?: number,
+ firstInstance?: number,
+ ): undefined;
+
+ drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined;
+ drawIndexedIndirect(
+ indirectBuffer: GPUBuffer,
+ indirectOffset: number,
+ ): undefined;
+}
+
+/** @category WebGPU */
+declare class GPURenderPassEncoder
+ implements GPUObjectBase, GPUProgrammablePassEncoder, GPURenderEncoderBase {
+ label: string;
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsets?: number[],
+ ): undefined;
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsetsData: Uint32Array,
+ dynamicOffsetsDataStart: number,
+ dynamicOffsetsDataLength: number,
+ ): undefined;
+ pushDebugGroup(groupLabel: string): undefined;
+ popDebugGroup(): undefined;
+ insertDebugMarker(markerLabel: string): undefined;
+ setPipeline(pipeline: GPURenderPipeline): undefined;
+ setIndexBuffer(
+ buffer: GPUBuffer,
+ indexFormat: GPUIndexFormat,
+ offset?: number,
+ size?: number,
+ ): undefined;
+ setVertexBuffer(
+ slot: number,
+ buffer: GPUBuffer,
+ offset?: number,
+ size?: number,
+ ): undefined;
+ draw(
+ vertexCount: number,
+ instanceCount?: number,
+ firstVertex?: number,
+ firstInstance?: number,
+ ): undefined;
+ drawIndexed(
+ indexCount: number,
+ instanceCount?: number,
+ firstIndex?: number,
+ baseVertex?: number,
+ firstInstance?: number,
+ ): undefined;
+ drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined;
+ drawIndexedIndirect(
+ indirectBuffer: GPUBuffer,
+ indirectOffset: number,
+ ): undefined;
+
+ setViewport(
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ minDepth: number,
+ maxDepth: number,
+ ): undefined;
+
+ setScissorRect(
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ ): undefined;
+
+ setBlendConstant(color: GPUColor): undefined;
+ setStencilReference(reference: number): undefined;
+
+ beginOcclusionQuery(queryIndex: number): undefined;
+ endOcclusionQuery(): undefined;
+
+ executeBundles(bundles: GPURenderBundle[]): undefined;
+ end(): undefined;
+}
+
+/** @category WebGPU */
+declare interface GPURenderPassTimestampWrites {
+ querySet: GPUQuerySet;
+ beginningOfPassWriteIndex?: number;
+ endOfPassWriteIndex?: number;
+}
+
+/** @category WebGPU */
+declare interface GPURenderPassDescriptor extends GPUObjectDescriptorBase {
+ colorAttachments: (GPURenderPassColorAttachment | null)[];
+ depthStencilAttachment?: GPURenderPassDepthStencilAttachment;
+ occlusionQuerySet?: GPUQuerySet;
+ timestampWrites?: GPURenderPassTimestampWrites;
+}
+
+/** @category WebGPU */
+declare interface GPURenderPassColorAttachment {
+ view: GPUTextureView;
+ resolveTarget?: GPUTextureView;
+
+ clearValue?: GPUColor;
+ loadOp: GPULoadOp;
+ storeOp: GPUStoreOp;
+}
+
+/** @category WebGPU */
+declare interface GPURenderPassDepthStencilAttachment {
+ view: GPUTextureView;
+
+ depthClearValue?: number;
+ depthLoadOp?: GPULoadOp;
+ depthStoreOp?: GPUStoreOp;
+ depthReadOnly?: boolean;
+
+ stencilClearValue?: number;
+ stencilLoadOp?: GPULoadOp;
+ stencilStoreOp?: GPUStoreOp;
+ stencilReadOnly?: boolean;
+}
+
+/** @category WebGPU */
+declare type GPULoadOp = "load" | "clear";
+
+/** @category WebGPU */
+declare type GPUStoreOp = "store" | "discard";
+
+/** @category WebGPU */
+declare class GPURenderBundle implements GPUObjectBase {
+ label: string;
+}
+
+/** @category WebGPU */
+declare interface GPURenderBundleDescriptor extends GPUObjectDescriptorBase {}
+
+/** @category WebGPU */
+declare class GPURenderBundleEncoder
+ implements GPUObjectBase, GPUProgrammablePassEncoder, GPURenderEncoderBase {
+ label: string;
+ draw(
+ vertexCount: number,
+ instanceCount?: number,
+ firstVertex?: number,
+ firstInstance?: number,
+ ): undefined;
+ drawIndexed(
+ indexCount: number,
+ instanceCount?: number,
+ firstIndex?: number,
+ baseVertex?: number,
+ firstInstance?: number,
+ ): undefined;
+ drawIndexedIndirect(
+ indirectBuffer: GPUBuffer,
+ indirectOffset: number,
+ ): undefined;
+ drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined;
+ insertDebugMarker(markerLabel: string): undefined;
+ popDebugGroup(): undefined;
+ pushDebugGroup(groupLabel: string): undefined;
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsets?: number[],
+ ): undefined;
+ setBindGroup(
+ index: number,
+ bindGroup: GPUBindGroup,
+ dynamicOffsetsData: Uint32Array,
+ dynamicOffsetsDataStart: number,
+ dynamicOffsetsDataLength: number,
+ ): undefined;
+ setIndexBuffer(
+ buffer: GPUBuffer,
+ indexFormat: GPUIndexFormat,
+ offset?: number,
+ size?: number,
+ ): undefined;
+ setPipeline(pipeline: GPURenderPipeline): undefined;
+ setVertexBuffer(
+ slot: number,
+ buffer: GPUBuffer,
+ offset?: number,
+ size?: number,
+ ): undefined;
+
+ finish(descriptor?: GPURenderBundleDescriptor): GPURenderBundle;
+}
+
+/** @category WebGPU */
+declare interface GPURenderPassLayout extends GPUObjectDescriptorBase {
+ colorFormats: (GPUTextureFormat | null)[];
+ depthStencilFormat?: GPUTextureFormat;
+ sampleCount?: number;
+}
+
+/** @category WebGPU */
+declare interface GPURenderBundleEncoderDescriptor extends GPURenderPassLayout {
+ depthReadOnly?: boolean;
+ stencilReadOnly?: boolean;
+}
+
+/** @category WebGPU */
+declare class GPUQueue implements GPUObjectBase {
+ label: string;
+
+ submit(commandBuffers: GPUCommandBuffer[]): undefined;
+
+ onSubmittedWorkDone(): Promise;
+
+ writeBuffer(
+ buffer: GPUBuffer,
+ bufferOffset: number,
+ data: BufferSource,
+ dataOffset?: number,
+ size?: number,
+ ): undefined;
+
+ writeTexture(
+ destination: GPUImageCopyTexture,
+ data: BufferSource,
+ dataLayout: GPUImageDataLayout,
+ size: GPUExtent3D,
+ ): undefined;
+}
+
+/** @category WebGPU */
+declare class GPUQuerySet implements GPUObjectBase {
+ label: string;
+
+ destroy(): undefined;
+
+ readonly type: GPUQueryType;
+ readonly count: number;
+}
+
+/** @category WebGPU */
+declare interface GPUQuerySetDescriptor extends GPUObjectDescriptorBase {
+ type: GPUQueryType;
+ count: number;
+}
+
+/** @category WebGPU */
+declare type GPUQueryType = "occlusion" | "timestamp";
+
+/** @category WebGPU */
+declare type GPUDeviceLostReason = "destroyed";
+
+/** @category WebGPU */
+declare interface GPUDeviceLostInfo {
+ readonly reason: GPUDeviceLostReason;
+ readonly message: string;
+}
+
+/** @category WebGPU */
+declare class GPUError {
+ readonly message: string;
+}
+
+/** @category WebGPU */
+declare class GPUOutOfMemoryError extends GPUError {
+ constructor(message: string);
+}
+
+/** @category WebGPU */
+declare class GPUValidationError extends GPUError {
+ constructor(message: string);
+}
+
+/** @category WebGPU */
+declare type GPUErrorFilter = "out-of-memory" | "validation";
+
+/** @category WebGPU */
+declare interface GPUColorDict {
+ r: number;
+ g: number;
+ b: number;
+ a: number;
+}
+
+/** @category WebGPU */
+declare type GPUColor = number[] | GPUColorDict;
+
+/** @category WebGPU */
+declare interface GPUOrigin3DDict {
+ x?: number;
+ y?: number;
+ z?: number;
+}
+
+/** @category WebGPU */
+declare type GPUOrigin3D = number[] | GPUOrigin3DDict;
+
+/** @category WebGPU */
+declare interface GPUExtent3DDict {
+ width: number;
+ height?: number;
+ depthOrArrayLayers?: number;
+}
+
+/** @category WebGPU */
+declare type GPUExtent3D = number[] | GPUExtent3DDict;
+
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
// deno-lint-ignore-file no-explicit-any no-var
///
@@ -9934,6 +11424,7 @@ declare interface CacheQueryOptions {
///
///
///
+///
///
///
///
@@ -10033,6 +11524,7 @@ declare var caches: CacheStorage;
/** @category Web APIs */
declare interface Navigator {
+ readonly gpu: GPU;
readonly hardwareConcurrency: number;
readonly userAgent: string;
readonly language: string;
@@ -11077,7 +12569,7 @@ declare namespace Deno {
*
* @category Fetch API
*/
- export interface HttpClient {
+ export interface HttpClient extends Disposable {
/** The resource ID associated with the client. */
rid: number;
/** Close the HTTP client. */
@@ -11547,6 +13039,32 @@ declare namespace Deno {
*/
export function openKv(path?: string): Promise;
+ /** **UNSTABLE**: New API, yet to be vetted.
+ *
+ * CronScheduleExpression is used as the type of `minute`, `hour`,
+ * `dayOfMonth`, `month`, and `dayOfWeek` in {@linkcode CronSchedule}.
+ * @category Cron
+ */
+ type CronScheduleExpression = number | { exact: number | number[] } | {
+ start?: number;
+ end?: number;
+ every?: number;
+ };
+
+ /** **UNSTABLE**: New API, yet to be vetted.
+ *
+ * CronSchedule is the interface used for JSON format
+ * cron `schedule`.
+ * @category Cron
+ */
+ export interface CronSchedule {
+ minute?: CronScheduleExpression;
+ hour?: CronScheduleExpression;
+ dayOfMonth?: CronScheduleExpression;
+ month?: CronScheduleExpression;
+ dayOfWeek?: CronScheduleExpression;
+ }
+
/** **UNSTABLE**: New API, yet to be vetted.
*
* Create a cron job that will periodically execute the provided handler
@@ -11557,21 +13075,23 @@ declare namespace Deno {
* console.log("cron job executed");
* });
* ```
- * `backoffSchedule` option can be used to specify the retry policy for failed
- * executions. Each element in the array represents the number of milliseconds
- * to wait before retrying the execution. For example, `[1000, 5000, 10000]`
- * means that a failed execution will be retried at most 3 times, with 1
- * second, 5 seconds, and 10 seconds delay between each retry.
+ *
+ * ```ts
+ * Deno.cron("sample cron", { hour: { every: 6 } }, () => {
+ * console.log("cron job executed");
+ * });
+ * ```
+ *
+ * `schedule` can be a string in the Unix cron format or in JSON format
+ * as specified by interface {@linkcode CronSchedule}, where time is specified
+ * using UTC time zone.
*
* @category Cron
- * @deprecated Use other {@linkcode cron} overloads instead. This overload
- * will be removed in the future.
*/
export function cron(
name: string,
- schedule: string,
+ schedule: string | CronSchedule,
handler: () => Promise | void,
- options: { backoffSchedule?: number[]; signal?: AbortSignal },
): Promise;
/** **UNSTABLE**: New API, yet to be vetted.
@@ -11580,19 +13100,29 @@ declare namespace Deno {
* callback based on the specified schedule.
*
* ```ts
- * Deno.cron("sample cron", "20 * * * *", () => {
+ * Deno.cron("sample cron", "20 * * * *", {
+ * backoffSchedule: [10, 20]
+ * }, () => {
* console.log("cron job executed");
* });
* ```
*
- * `schedule` is a Unix cron format expression, where time is specified
+ * `schedule` can be a string in the Unix cron format or in JSON format
+ * as specified by interface {@linkcode CronSchedule}, where time is specified
* using UTC time zone.
*
+ * `backoffSchedule` option can be used to specify the retry policy for failed
+ * executions. Each element in the array represents the number of milliseconds
+ * to wait before retrying the execution. For example, `[1000, 5000, 10000]`
+ * means that a failed execution will be retried at most 3 times, with 1
+ * second, 5 seconds, and 10 seconds delay between each retry.
+ *
* @category Cron
*/
export function cron(
name: string,
- schedule: string,
+ schedule: string | CronSchedule,
+ options: { backoffSchedule?: number[]; signal?: AbortSignal },
handler: () => Promise | void,
): Promise;
@@ -11601,17 +13131,15 @@ declare namespace Deno {
* Create a cron job that will periodically execute the provided handler
* callback based on the specified schedule.
*
+ * `schedule` can be a string in the Unix cron format or in JSON format
+ * as specified by interface {@linkcode CronSchedule}, where time is specified
+ * using UTC time zone.
+ *
* ```ts
- * Deno.cron("sample cron", "20 * * * *", {
- * backoffSchedule: [10, 20]
- * }, () => {
+ * Deno.cron("sample cron", "20 * * * *", () => {
* console.log("cron job executed");
* });
* ```
- *
- * `schedule` is a Unix cron format expression, where time is specified
- * using UTC time zone.
- *
* `backoffSchedule` option can be used to specify the retry policy for failed
* executions. Each element in the array represents the number of milliseconds
* to wait before retrying the execution. For example, `[1000, 5000, 10000]`
@@ -11619,12 +13147,14 @@ declare namespace Deno {
* second, 5 seconds, and 10 seconds delay between each retry.
*
* @category Cron
+ * @deprecated Use other {@linkcode cron} overloads instead. This overload
+ * will be removed in the future.
*/
export function cron(
name: string,
- schedule: string,
- options: { backoffSchedule?: number[]; signal?: AbortSignal },
+ schedule: string | CronSchedule,
handler: () => Promise | void,
+ options: { backoffSchedule?: number[]; signal?: AbortSignal },
): Promise;
/** **UNSTABLE**: New API, yet to be vetted.
@@ -11680,7 +13210,13 @@ declare namespace Deno {
*
* @category KV
*/
- export type KvKeyPart = Uint8Array | string | number | bigint | boolean;
+ export type KvKeyPart =
+ | Uint8Array
+ | string
+ | number
+ | bigint
+ | boolean
+ | symbol;
/** **UNSTABLE**: New API, yet to be vetted.
*
@@ -11978,7 +13514,11 @@ declare namespace Deno {
*/
enqueue(
value: unknown,
- options?: { delay?: number; keysIfUndelivered?: Deno.KvKey[] },
+ options?: {
+ delay?: number;
+ keysIfUndelivered?: Deno.KvKey[];
+ backoffSchedule?: number[];
+ },
): this;
/**
* Commit the operation to the KV store. Returns a value indicating whether
@@ -12187,14 +13727,28 @@ declare namespace Deno {
* listener after several attempts. The values are set to the value of
* the queued message.
*
+ * The `backoffSchedule` option can be used to specify the retry policy for
+ * failed message delivery. Each element in the array represents the number of
+ * milliseconds to wait before retrying the delivery. For example,
+ * `[1000, 5000, 10000]` means that a failed delivery will be retried
+ * at most 3 times, with 1 second, 5 seconds, and 10 seconds delay
+ * between each retry.
+ *
* ```ts
* const db = await Deno.openKv();
- * await db.enqueue("bar", { keysIfUndelivered: [["foo", "bar"]] });
+ * await db.enqueue("bar", {
+ * keysIfUndelivered: [["foo", "bar"]],
+ * backoffSchedule: [1000, 5000, 10000],
+ * });
* ```
*/
enqueue(
value: unknown,
- options?: { delay?: number; keysIfUndelivered?: Deno.KvKey[] },
+ options?: {
+ delay?: number;
+ keysIfUndelivered?: Deno.KvKey[];
+ backoffSchedule?: number[];
+ },
): Promise;
/**
@@ -12273,6 +13827,14 @@ declare namespace Deno {
*/
close(): void;
+ /**
+ * Get a symbol that represents the versionstamp of the current atomic
+ * operation. This symbol can be used as the last part of a key in
+ * `.set()`, both directly on the `Kv` object and on an `AtomicOperation`
+ * object created from this `Kv` instance.
+ */
+ commitVersionstamp(): symbol;
+
[Symbol.dispose](): void;
}
@@ -12291,138 +13853,6 @@ declare namespace Deno {
readonly value: bigint;
}
- /** An instance of the server created using `Deno.serve()` API.
- *
- * @category HTTP Server
- */
- export interface HttpServer {
- /** Gracefully close the server. No more new connections will be accepted,
- * while pending requests will be allowed to finish.
- */
- shutdown(): Promise;
- }
-
- export interface ServeUnixOptions {
- /** The unix domain socket path to listen on. */
- path: string;
-
- /** An {@linkcode AbortSignal} to close the server and all connections. */
- signal?: AbortSignal;
-
- /** The handler to invoke when route handlers throw an error. */
- onError?: (error: unknown) => Response | Promise;
-
- /** The callback which is called when the server starts listening. */
- onListen?: (params: { path: string }) => void;
- }
-
- /** Information for a unix domain socket HTTP request.
- *
- * @category HTTP Server
- */
- export interface ServeUnixHandlerInfo {
- /** The remote address of the connection. */
- remoteAddr: Deno.UnixAddr;
- }
-
- /** A handler for unix domain socket HTTP requests. Consumes a request and returns a response.
- *
- * If a handler throws, the server calling the handler will assume the impact
- * of the error is isolated to the individual request. It will catch the error
- * and if necessary will close the underlying connection.
- *
- * @category HTTP Server
- */
- export type ServeUnixHandler = (
- request: Request,
- info: ServeUnixHandlerInfo,
- ) => Response | Promise;
-
- /**
- * @category HTTP Server
- */
- export interface ServeUnixInit {
- /** The handler to invoke to process each incoming request. */
- handler: ServeUnixHandler;
- }
-
- /** Serves HTTP requests with the given option bag and handler.
- *
- * You can specify the socket path with `path` option.
- *
- * ```ts
- * Deno.serve(
- * { path: "path/to/socket" },
- * (_req) => new Response("Hello, world")
- * );
- * ```
- *
- * You can stop the server with an {@linkcode AbortSignal}. The abort signal
- * needs to be passed as the `signal` option in the options bag. The server
- * aborts when the abort signal is aborted. To wait for the server to close,
- * await the promise returned from the `Deno.serve` API.
- *
- * ```ts
- * const ac = new AbortController();
- *
- * const server = Deno.serve(
- * { signal: ac.signal, path: "path/to/socket" },
- * (_req) => new Response("Hello, world")
- * );
- * server.finished.then(() => console.log("Server closed"));
- *
- * console.log("Closing server...");
- * ac.abort();
- * ```
- *
- * By default `Deno.serve` prints the message
- * `Listening on path/to/socket` on listening. If you like to
- * change this behavior, you can specify a custom `onListen` callback.
- *
- * ```ts
- * Deno.serve({
- * onListen({ path }) {
- * console.log(`Server started at ${path}`);
- * // ... more info specific to your server ..
- * },
- * path: "path/to/socket",
- * }, (_req) => new Response("Hello, world"));
- * ```
- *
- * @category HTTP Server
- */
- export function serve(
- options: ServeUnixOptions,
- handler: ServeUnixHandler,
- ): Server;
- /** Serves HTTP requests with the given option bag.
- *
- * You can specify an object with the path option, which is the
- * unix domain socket to listen on.
- *
- * ```ts
- * const ac = new AbortController();
- *
- * const server = Deno.serve({
- * path: "path/to/socket",
- * handler: (_req) => new Response("Hello, world"),
- * signal: ac.signal,
- * onListen({ path }) {
- * console.log(`Server started at ${path}`);
- * },
- * });
- * server.finished.then(() => console.log("Server closed"));
- *
- * console.log("Closing server...");
- * ac.abort();
- * ```
- *
- * @category HTTP Server
- */
- export function serve(
- options: ServeUnixInit & ServeUnixOptions,
- ): Server;
-
/**
* A namespace containing runtime APIs available in Jupyter notebooks.
*
diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts
index 52a878bdde17..bd2a7061019a 100644
--- a/packages/deno/src/index.ts
+++ b/packages/deno/src/index.ts
@@ -31,6 +31,7 @@ export {
captureEvent,
captureMessage,
close,
+ // eslint-disable-next-line deprecation/deprecation
configureScope,
createTransport,
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/deno/src/integrations/deno-cron-format.ts b/packages/deno/src/integrations/deno-cron-format.ts
new file mode 100644
index 000000000000..ac7bfc813fde
--- /dev/null
+++ b/packages/deno/src/integrations/deno-cron-format.ts
@@ -0,0 +1,84 @@
+/**
+ * These functions were copied from the Deno source code here:
+ * https://github.com/denoland/deno/blob/cd480b481ee1b4209910aa7a8f81ffa996e7b0f9/ext/cron/01_cron.ts
+ * Below is the original license:
+ *
+ * MIT License
+ *
+ * Copyright 2018-2023 the Deno authors
+ *
+ * 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.
+ */
+
+function formatToCronSchedule(
+ value?:
+ | number
+ | { exact: number | number[] }
+ | {
+ start?: number;
+ end?: number;
+ every?: number;
+ },
+): string {
+ if (value === undefined) {
+ return '*';
+ } else if (typeof value === 'number') {
+ return value.toString();
+ } else {
+ const { exact } = value as { exact: number | number[] };
+ if (exact === undefined) {
+ const { start, end, every } = value as {
+ start?: number;
+ end?: number;
+ every?: number;
+ };
+ if (start !== undefined && end !== undefined && every !== undefined) {
+ return `${start}-${end}/${every}`;
+ } else if (start !== undefined && end !== undefined) {
+ return `${start}-${end}`;
+ } else if (start !== undefined && every !== undefined) {
+ return `${start}/${every}`;
+ } else if (start !== undefined) {
+ return `${start}/1`;
+ } else if (end === undefined && every !== undefined) {
+ return `*/${every}`;
+ } else {
+ throw new TypeError('Invalid cron schedule');
+ }
+ } else {
+ if (typeof exact === 'number') {
+ return exact.toString();
+ } else {
+ return exact.join(',');
+ }
+ }
+ }
+}
+
+/** */
+export function parseScheduleToString(schedule: string | Deno.CronSchedule): string {
+ if (typeof schedule === 'string') {
+ return schedule;
+ } else {
+ const { minute, hour, dayOfMonth, month, dayOfWeek } = schedule;
+
+ return `${formatToCronSchedule(minute)} ${formatToCronSchedule(hour)} ${formatToCronSchedule(
+ dayOfMonth,
+ )} ${formatToCronSchedule(month)} ${formatToCronSchedule(dayOfWeek)}`;
+ }
+}
diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts
index f40d696f5e3c..475d3e9131b7 100644
--- a/packages/deno/src/integrations/deno-cron.ts
+++ b/packages/deno/src/integrations/deno-cron.ts
@@ -1,11 +1,12 @@
import { withMonitor } from '@sentry/core';
import type { Integration } from '@sentry/types';
import type { DenoClient } from '../client';
+import { parseScheduleToString } from './deno-cron-format';
type CronOptions = { backoffSchedule?: number[]; signal?: AbortSignal };
type CronFn = () => void | Promise;
// Parameters doesn't work well with the overloads 🤔
-type CronParams = [string, string, CronFn | CronOptions, CronFn | CronOptions | undefined];
+type CronParams = [string, string | Deno.CronSchedule, CronFn | CronOptions, CronFn | CronOptions | undefined];
/** Instruments Deno.cron to automatically capture cron check-ins */
export class DenoCron implements Integration {
@@ -21,7 +22,7 @@ export class DenoCron implements Integration {
}
/** @inheritDoc */
- public setup(client: DenoClient): void {
+ public setup(): void {
// eslint-disable-next-line deprecation/deprecation
if (!Deno.cron) {
// The cron API is not available in this Deno version use --unstable flag!
@@ -45,9 +46,11 @@ export class DenoCron implements Integration {
async function cronCalled(): Promise {
await withMonitor(monitorSlug, async () => fn(), {
- schedule: { type: 'crontab', value: schedule },
+ schedule: { type: 'crontab', value: parseScheduleToString(schedule) },
// (minutes) so 12 hours - just a very high arbitrary number since we don't know the actual duration of the users cron job
maxRuntime: 60 * 12,
+ // Deno Deploy docs say that the cron job will be called within 1 minute of the scheduled time
+ checkinMargin: 1,
});
}
diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts
index d173780cfa50..27745d6d6765 100644
--- a/packages/deno/src/integrations/globalhandlers.ts
+++ b/packages/deno/src/integrations/globalhandlers.ts
@@ -1,6 +1,8 @@
import type { ServerRuntimeClient } from '@sentry/core';
-import { flush, getCurrentHub } from '@sentry/core';
-import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types';
+import { captureEvent } from '@sentry/core';
+import { getClient } from '@sentry/core';
+import { flush } from '@sentry/core';
+import type { Client, Event, Integration, Primitive, StackParser } from '@sentry/types';
import { eventFromUnknownInput, isPrimitive } from '@sentry/utils';
type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection';
@@ -25,15 +27,6 @@ export class GlobalHandlers implements Integration {
/** JSDoc */
private readonly _options: GlobalHandlersIntegrations;
- /**
- * Stores references functions to installing handlers. Will set to undefined
- * after they have been run so that they are not used twice.
- */
- private _installFunc: Record void) | undefined> = {
- error: installGlobalErrorHandler,
- unhandledrejection: installGlobalUnhandledRejectionHandler,
- };
-
/** JSDoc */
public constructor(options?: GlobalHandlersIntegrations) {
this._options = {
@@ -46,35 +39,35 @@ export class GlobalHandlers implements Integration {
* @inheritDoc
*/
public setupOnce(): void {
- const options = this._options;
-
- // We can disable guard-for-in as we construct the options object above + do checks against
- // `this._installFunc` for the property.
- // eslint-disable-next-line guard-for-in
- for (const key in options) {
- const installFunc = this._installFunc[key as GlobalHandlersIntegrationsOptionKeys];
- if (installFunc && options[key as GlobalHandlersIntegrationsOptionKeys]) {
- installFunc();
- this._installFunc[key as GlobalHandlersIntegrationsOptionKeys] = undefined;
- }
+ // noop
+ }
+
+ /** @inheritdoc */
+ public setup(client: Client): void {
+ if (this._options.error) {
+ installGlobalErrorHandler(client);
+ }
+ if (this._options.unhandledrejection) {
+ installGlobalUnhandledRejectionHandler(client);
}
}
}
-function installGlobalErrorHandler(): void {
+function installGlobalErrorHandler(client: Client): void {
globalThis.addEventListener('error', data => {
- if (isExiting) {
+ if (getClient() !== client || isExiting) {
return;
}
- const [hub, stackParser] = getHubAndOptions();
+ const stackParser = getStackParser();
+
const { message, error } = data;
- const event = eventFromUnknownInput(getCurrentHub, stackParser, error || message);
+ const event = eventFromUnknownInput(getClient(), stackParser, error || message);
event.level = 'fatal';
- hub.captureEvent(event, {
+ captureEvent(event, {
originalException: error,
mechanism: {
handled: false,
@@ -93,13 +86,13 @@ function installGlobalErrorHandler(): void {
});
}
-function installGlobalUnhandledRejectionHandler(): void {
+function installGlobalUnhandledRejectionHandler(client: Client): void {
globalThis.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
- if (isExiting) {
+ if (getClient() !== client || isExiting) {
return;
}
- const [hub, stackParser] = getHubAndOptions();
+ const stackParser = getStackParser();
let error = e;
// dig the object of the rejection out of known event types
@@ -113,11 +106,11 @@ function installGlobalUnhandledRejectionHandler(): void {
const event = isPrimitive(error)
? eventFromRejectionWithPrimitive(error)
- : eventFromUnknownInput(getCurrentHub, stackParser, error, undefined);
+ : eventFromUnknownInput(getClient(), stackParser, error, undefined);
event.level = 'fatal';
- hub.captureEvent(event, {
+ captureEvent(event, {
originalException: error,
mechanism: {
handled: false,
@@ -156,12 +149,12 @@ function eventFromRejectionWithPrimitive(reason: Primitive): Event {
};
}
-function getHubAndOptions(): [Hub, StackParser] {
- const hub = getCurrentHub();
- const client = hub.getClient();
- const options = (client && client.getOptions()) || {
- stackParser: () => [],
- attachStacktrace: false,
- };
- return [hub, options.stackParser];
+function getStackParser(): StackParser {
+ const client = getClient();
+
+ if (!client) {
+ return () => [];
+ }
+
+ return client.getOptions().stackParser;
}
diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts
index ed4372065792..7585c88f0ab1 100644
--- a/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts
+++ b/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts
@@ -4,7 +4,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' });
- Sentry.getCurrentHub().configureScope(scope => scope.setSpan(transaction));
+ Sentry.getCurrentHub().getScope().setSpan(transaction);
const span = transaction.startChild();
diff --git a/packages/e2e-tests/test-applications/nextjs-14/.gitignore b/packages/e2e-tests/test-applications/nextjs-14/.gitignore
new file mode 100644
index 000000000000..e799cc33c4e7
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/.gitignore
@@ -0,0 +1,45 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+!*.d.ts
+
+# Sentry
+.sentryclirc
+
+.vscode
+
+test-results
diff --git a/packages/e2e-tests/test-applications/nextjs-14/.npmrc b/packages/e2e-tests/test-applications/nextjs-14/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx b/packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx
new file mode 100644
index 000000000000..5ae73102057d
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx
@@ -0,0 +1,33 @@
+export const dynamic = 'force-dynamic';
+
+export default function Page() {
+ return Hello World!
;
+}
+
+export async function generateMetadata({
+ searchParams,
+}: {
+ searchParams: { [key: string]: string | string[] | undefined };
+}) {
+ if (searchParams['shouldThrowInGenerateMetadata']) {
+ throw new Error('generateMetadata Error');
+ }
+
+ return {
+ title: searchParams['metadataTitle'] ?? 'not set',
+ };
+}
+
+export function generateViewport({
+ searchParams,
+}: {
+ searchParams: { [key: string]: string | undefined };
+}) {
+ if (searchParams['shouldThrowInGenerateViewport']) {
+ throw new Error('generateViewport Error');
+ }
+
+ return {
+ themeColor: searchParams['viewportThemeColor'] ?? 'black',
+ };
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-14/app/layout.tsx b/packages/e2e-tests/test-applications/nextjs-14/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/app/layout.tsx
@@ -0,0 +1,7 @@
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts b/packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts
new file mode 100644
index 000000000000..9dee679c71e4
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts
@@ -0,0 +1,253 @@
+import * as fs from 'fs';
+import * as http from 'http';
+import * as https from 'https';
+import type { AddressInfo } from 'net';
+import * as os from 'os';
+import * as path from 'path';
+import * as util from 'util';
+import * as zlib from 'zlib';
+import type { Envelope, EnvelopeItem, Event } from '@sentry/types';
+import { parseEnvelope } from '@sentry/utils';
+
+const readFile = util.promisify(fs.readFile);
+const writeFile = util.promisify(fs.writeFile);
+
+interface EventProxyServerOptions {
+ /** Port to start the event proxy server at. */
+ port: number;
+ /** The name for the proxy server used for referencing it with listener functions */
+ proxyServerName: string;
+}
+
+interface SentryRequestCallbackData {
+ envelope: Envelope;
+ rawProxyRequestBody: string;
+ rawSentryResponseBody: string;
+ sentryResponseStatusCode?: number;
+}
+
+/**
+ * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel`
+ * option to this server (like this `tunnel: http://localhost:${port option}/`).
+ */
+export async function startEventProxyServer(options: EventProxyServerOptions): Promise {
+ const eventCallbackListeners: Set<(data: string) => void> = new Set();
+
+ const proxyServer = http.createServer((proxyRequest, proxyResponse) => {
+ const proxyRequestChunks: Uint8Array[] = [];
+
+ proxyRequest.addListener('data', (chunk: Buffer) => {
+ proxyRequestChunks.push(chunk);
+ });
+
+ proxyRequest.addListener('error', err => {
+ throw err;
+ });
+
+ proxyRequest.addListener('end', () => {
+ const proxyRequestBody =
+ proxyRequest.headers['content-encoding'] === 'gzip'
+ ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString()
+ : Buffer.concat(proxyRequestChunks).toString();
+
+ let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]);
+
+ if (!envelopeHeader.dsn) {
+ throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.');
+ }
+
+ const { origin, pathname, host } = new URL(envelopeHeader.dsn);
+
+ const projectId = pathname.substring(1);
+ const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`;
+
+ proxyRequest.headers.host = host;
+
+ const sentryResponseChunks: Uint8Array[] = [];
+
+ const sentryRequest = https.request(
+ sentryIngestUrl,
+ { headers: proxyRequest.headers, method: proxyRequest.method },
+ sentryResponse => {
+ sentryResponse.addListener('data', (chunk: Buffer) => {
+ proxyResponse.write(chunk, 'binary');
+ sentryResponseChunks.push(chunk);
+ });
+
+ sentryResponse.addListener('end', () => {
+ eventCallbackListeners.forEach(listener => {
+ const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString();
+
+ const data: SentryRequestCallbackData = {
+ envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()),
+ rawProxyRequestBody: proxyRequestBody,
+ rawSentryResponseBody,
+ sentryResponseStatusCode: sentryResponse.statusCode,
+ };
+
+ listener(Buffer.from(JSON.stringify(data)).toString('base64'));
+ });
+ proxyResponse.end();
+ });
+
+ sentryResponse.addListener('error', err => {
+ throw err;
+ });
+
+ proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers);
+ },
+ );
+
+ sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary');
+ sentryRequest.end();
+ });
+ });
+
+ const proxyServerStartupPromise = new Promise(resolve => {
+ proxyServer.listen(options.port, () => {
+ resolve();
+ });
+ });
+
+ const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => {
+ eventCallbackResponse.statusCode = 200;
+ eventCallbackResponse.setHeader('connection', 'keep-alive');
+
+ const callbackListener = (data: string): void => {
+ eventCallbackResponse.write(data.concat('\n'), 'utf8');
+ };
+
+ eventCallbackListeners.add(callbackListener);
+
+ eventCallbackRequest.on('close', () => {
+ eventCallbackListeners.delete(callbackListener);
+ });
+
+ eventCallbackRequest.on('error', () => {
+ eventCallbackListeners.delete(callbackListener);
+ });
+ });
+
+ const eventCallbackServerStartupPromise = new Promise(resolve => {
+ eventCallbackServer.listen(0, () => {
+ const port = String((eventCallbackServer.address() as AddressInfo).port);
+ void registerCallbackServerPort(options.proxyServerName, port).then(resolve);
+ });
+ });
+
+ await eventCallbackServerStartupPromise;
+ await proxyServerStartupPromise;
+ return;
+}
+
+export async function waitForRequest(
+ proxyServerName: string,
+ callback: (eventData: SentryRequestCallbackData) => Promise | boolean,
+): Promise {
+ const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName);
+
+ return new Promise((resolve, reject) => {
+ const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
+ let eventContents = '';
+
+ response.on('error', err => {
+ reject(err);
+ });
+
+ response.on('data', (chunk: Buffer) => {
+ const chunkString = chunk.toString('utf8');
+ chunkString.split('').forEach(char => {
+ if (char === '\n') {
+ const eventCallbackData: SentryRequestCallbackData = JSON.parse(
+ Buffer.from(eventContents, 'base64').toString('utf8'),
+ );
+ const callbackResult = callback(eventCallbackData);
+ if (typeof callbackResult !== 'boolean') {
+ callbackResult.then(
+ match => {
+ if (match) {
+ response.destroy();
+ resolve(eventCallbackData);
+ }
+ },
+ err => {
+ throw err;
+ },
+ );
+ } else if (callbackResult) {
+ response.destroy();
+ resolve(eventCallbackData);
+ }
+ eventContents = '';
+ } else {
+ eventContents = eventContents.concat(char);
+ }
+ });
+ });
+ });
+
+ request.end();
+ });
+}
+
+export function waitForEnvelopeItem(
+ proxyServerName: string,
+ callback: (envelopeItem: EnvelopeItem) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForRequest(proxyServerName, async eventData => {
+ const envelopeItems = eventData.envelope[1];
+ for (const envelopeItem of envelopeItems) {
+ if (await callback(envelopeItem)) {
+ resolve(envelopeItem);
+ return true;
+ }
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+export function waitForError(
+ proxyServerName: string,
+ callback: (transactionEvent: Event) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForEnvelopeItem(proxyServerName, async envelopeItem => {
+ const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
+ if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
+ resolve(envelopeItemBody as Event);
+ return true;
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+export function waitForTransaction(
+ proxyServerName: string,
+ callback: (transactionEvent: Event) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForEnvelopeItem(proxyServerName, async envelopeItem => {
+ const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
+ if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
+ resolve(envelopeItemBody as Event);
+ return true;
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+const TEMP_FILE_PREFIX = 'event-proxy-server-';
+
+async function registerCallbackServerPort(serverName: string, port: string): Promise {
+ const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
+ await writeFile(tmpFilePath, port, { encoding: 'utf8' });
+}
+
+function retrieveCallbackServerPort(serverName: string): Promise {
+ const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
+ return readFile(tmpFilePath, 'utf8');
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-14/globals.d.ts b/packages/e2e-tests/test-applications/nextjs-14/globals.d.ts
new file mode 100644
index 000000000000..109dbcd55648
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/globals.d.ts
@@ -0,0 +1,4 @@
+interface Window {
+ recordedTransactions?: string[];
+ capturedExceptionId?: string;
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-14/next-env.d.ts b/packages/e2e-tests/test-applications/nextjs-14/next-env.d.ts
new file mode 100644
index 000000000000..4f11a03dc6cc
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/packages/e2e-tests/test-applications/nextjs-14/next.config.js b/packages/e2e-tests/test-applications/nextjs-14/next.config.js
new file mode 100644
index 000000000000..4beb4fc356f4
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/next.config.js
@@ -0,0 +1,30 @@
+// This file sets a custom webpack configuration to use your Next.js app
+// with Sentry.
+// https://nextjs.org/docs/api-reference/next.config.js/introduction
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+const { withSentryConfig } = require('@sentry/nextjs');
+
+/** @type {import('next').NextConfig} */
+const moduleExports = {};
+
+const sentryWebpackPluginOptions = {
+ // Additional config options for the Sentry Webpack plugin. Keep in mind that
+ // the following options are set automatically, and overriding them is not
+ // recommended:
+ // release, url, org, project, authToken, configFile, stripPrefix,
+ // urlPrefix, include, ignore
+
+ silent: true, // Suppresses all logs
+ // For all available options, see:
+ // https://github.com/getsentry/sentry-webpack-plugin#options.
+
+ // We're not testing source map uploads at the moment.
+ dryRun: true,
+};
+
+// Make sure adding Sentry options is the last code to run before exporting, to
+// ensure that your source maps include changes from all other Webpack plugins
+module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions, {
+ hideSourceMaps: true,
+});
diff --git a/packages/e2e-tests/test-applications/nextjs-14/package.json b/packages/e2e-tests/test-applications/nextjs-14/package.json
new file mode 100644
index 000000000000..b822f4316566
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "create-next-app",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr",
+ "clean": "npx rimraf node_modules,pnpm-lock.yaml",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:dev": "TEST_ENV=development playwright test",
+ "test:build": "pnpm install && npx playwright install && pnpm build",
+ "test:build-canary": "pnpm install && pnpm add next@canary && npx playwright install && pnpm build",
+ "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build",
+ "test:assert": "pnpm test:prod && pnpm test:dev"
+ },
+ "dependencies": {
+ "@sentry/nextjs": "latest || *",
+ "@types/node": "18.11.17",
+ "@types/react": "18.0.26",
+ "@types/react-dom": "18.0.9",
+ "next": "14.0.4",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "typescript": "4.9.5",
+ "wait-port": "1.0.4",
+ "ts-node": "10.9.1",
+ "@playwright/test": "^1.27.1"
+ },
+ "devDependencies": {
+ "@sentry/types": "latest || *",
+ "@sentry/utils": "latest || *"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts b/packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts
new file mode 100644
index 000000000000..ab3c40a21471
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts
@@ -0,0 +1,77 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+import { devices } from '@playwright/test';
+
+// Fix urls not resolving to localhost on Node v17+
+// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575
+import { setDefaultResultOrder } from 'dns';
+setDefaultResultOrder('ipv4first');
+
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const nextPort = 3030;
+const eventProxyPort = 3031;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: './tests',
+ /* Maximum time one test can run for. */
+ timeout: 150_000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 10000,
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* `next dev` is incredibly buggy with the app dir */
+ retries: testEnv === 'development' ? 3 : 0,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'list',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: `http://localhost:${nextPort}`,
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: [
+ {
+ command: 'pnpm ts-node-script start-event-proxy.ts',
+ port: eventProxyPort,
+ },
+ {
+ command:
+ testEnv === 'development'
+ ? `pnpm wait-port ${eventProxyPort} && pnpm next dev -p ${nextPort}`
+ : `pnpm wait-port ${eventProxyPort} && pnpm next start -p ${nextPort}`,
+ port: nextPort,
+ },
+ ],
+};
+
+export default config;
diff --git a/packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts b/packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts
new file mode 100644
index 000000000000..85bd765c9c44
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+});
diff --git a/packages/e2e-tests/test-applications/nextjs-14/sentry.edge.config.ts b/packages/e2e-tests/test-applications/nextjs-14/sentry.edge.config.ts
new file mode 100644
index 000000000000..85bd765c9c44
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/sentry.edge.config.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+});
diff --git a/packages/e2e-tests/test-applications/nextjs-14/sentry.server.config.ts b/packages/e2e-tests/test-applications/nextjs-14/sentry.server.config.ts
new file mode 100644
index 000000000000..85bd765c9c44
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/sentry.server.config.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+});
diff --git a/packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts b/packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts
new file mode 100644
index 000000000000..eb83fd6fb82d
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from './event-proxy-server';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nextjs-14',
+});
diff --git a/packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts
new file mode 100644
index 000000000000..3828312607ea
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts
@@ -0,0 +1,79 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '../event-proxy-server';
+
+test('Should send a transaction event for a generateMetadata() function invokation', async ({ page }) => {
+ const testTitle = 'foobarasdf';
+
+ const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions)' &&
+ transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle
+ );
+ });
+
+ await page.goto(`/generation-functions?metadataTitle=${testTitle}`);
+
+ expect(await transactionPromise).toBeDefined();
+
+ const pageTitle = await page.title();
+ expect(pageTitle).toBe(testTitle);
+});
+
+test('Should send a transaction and an error event for a faulty generateMetadata() function invokation', async ({
+ page,
+}) => {
+ const testTitle = 'foobarbaz';
+
+ const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions)' &&
+ transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle
+ );
+ });
+
+ const errorEventPromise = waitForError('nextjs-14', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'generateMetadata Error';
+ });
+
+ await page.goto(`/generation-functions?metadataTitle=${testTitle}&shouldThrowInGenerateMetadata=1`);
+
+ expect(await transactionPromise).toBeDefined();
+ expect(await errorEventPromise).toBeDefined();
+});
+
+test('Should send a transaction event for a generateViewport() function invokation', async ({ page }) => {
+ const testTitle = 'floob';
+
+ const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' &&
+ transactionEvent.contexts?.trace?.data?.['searchParams']?.['viewportThemeColor'] === testTitle
+ );
+ });
+
+ await page.goto(`/generation-functions?viewportThemeColor=${testTitle}`);
+
+ expect(await transactionPromise).toBeDefined();
+});
+
+test('Should send a transaction and an error event for a faulty generateViewport() function invokation', async ({
+ page,
+}) => {
+ const testTitle = 'blargh';
+
+ const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' &&
+ transactionEvent.contexts?.trace?.data?.['searchParams']?.['viewportThemeColor'] === testTitle
+ );
+ });
+
+ const errorEventPromise = waitForError('nextjs-14', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'generateViewport Error';
+ });
+
+ await page.goto(`/generation-functions?viewportThemeColor=${testTitle}&shouldThrowInGenerateViewport=1`);
+
+ expect(await transactionPromise).toBeDefined();
+ expect(await errorEventPromise).toBeDefined();
+});
diff --git a/packages/e2e-tests/test-applications/nextjs-14/tsconfig.json b/packages/e2e-tests/test-applications/nextjs-14/tsconfig.json
new file mode 100644
index 000000000000..60825545944d
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-14/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "incremental": true
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"],
+ "ts-node": {
+ "compilerOptions": {
+ "module": "CommonJS"
+ }
+ }
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/layout.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/layout.tsx
new file mode 100644
index 000000000000..ace0c2f086b7
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/layout.tsx
@@ -0,0 +1,12 @@
+import { PropsWithChildren } from 'react';
+
+export const dynamic = 'force-dynamic';
+
+export default function Layout({ children }: PropsWithChildren<{}>) {
+ return (
+
+ );
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/layout.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/layout.tsx
new file mode 100644
index 000000000000..ace0c2f086b7
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/layout.tsx
@@ -0,0 +1,12 @@
+import { PropsWithChildren } from 'react';
+
+export const dynamic = 'force-dynamic';
+
+export default function Layout({ children }: PropsWithChildren<{}>) {
+ return (
+
+ );
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/page.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/page.tsx
new file mode 100644
index 000000000000..8077c14d23ca
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/page.tsx
@@ -0,0 +1,11 @@
+export const dynamic = 'force-dynamic';
+
+export default function Page() {
+ return Hello World!
;
+}
+
+export async function generateMetadata() {
+ return {
+ title: 'I am generated metadata',
+ };
+}
diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
new file mode 100644
index 000000000000..4acc41814d3c
--- /dev/null
+++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
@@ -0,0 +1,50 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '../event-proxy-server';
+
+test('Will capture a connected trace for all server components and generation functions when visiting a page', async ({
+ page,
+}) => {
+ const someConnectedEvent = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' ||
+ transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' ||
+ transactionEvent?.transaction === 'Page Server Component (/(nested-layout)/nested-layout)' ||
+ transactionEvent?.transaction === 'Page.generateMetadata (/(nested-layout)/nested-layout)'
+ );
+ });
+
+ const layout1Transaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' &&
+ (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
+ );
+ });
+
+ const layout2Transaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' &&
+ (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
+ );
+ });
+
+ const pageTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Page Server Component (/(nested-layout)/nested-layout)' &&
+ (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
+ );
+ });
+
+ const generateMetadataTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'Page.generateMetadata (/(nested-layout)/nested-layout)' &&
+ (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
+ );
+ });
+
+ await page.goto('/nested-layout');
+
+ expect(await layout1Transaction).toBeDefined();
+ expect(await layout2Transaction).toBeDefined();
+ expect(await pageTransaction).toBeDefined();
+ expect(await generateMetadataTransaction).toBeDefined();
+});
diff --git a/packages/e2e-tests/test-applications/node-express-app/src/app.ts b/packages/e2e-tests/test-applications/node-express-app/src/app.ts
index e9de96631259..330a425cb494 100644
--- a/packages/e2e-tests/test-applications/node-express-app/src/app.ts
+++ b/packages/e2e-tests/test-applications/node-express-app/src/app.ts
@@ -35,7 +35,7 @@ app.get('/test-param/:param', function (req, res) {
app.get('/test-transaction', async function (req, res) {
const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' });
- Sentry.getCurrentHub().configureScope(scope => scope.setSpan(transaction));
+ Sentry.getCurrentScope().setSpan(transaction);
const span = transaction.startChild();
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/.gitignore b/packages/e2e-tests/test-applications/node-hapi-app/.gitignore
new file mode 100644
index 000000000000..1521c8b7652b
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/.gitignore
@@ -0,0 +1 @@
+dist
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/.npmrc b/packages/e2e-tests/test-applications/node-hapi-app/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts b/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts
new file mode 100644
index 000000000000..9dee679c71e4
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts
@@ -0,0 +1,253 @@
+import * as fs from 'fs';
+import * as http from 'http';
+import * as https from 'https';
+import type { AddressInfo } from 'net';
+import * as os from 'os';
+import * as path from 'path';
+import * as util from 'util';
+import * as zlib from 'zlib';
+import type { Envelope, EnvelopeItem, Event } from '@sentry/types';
+import { parseEnvelope } from '@sentry/utils';
+
+const readFile = util.promisify(fs.readFile);
+const writeFile = util.promisify(fs.writeFile);
+
+interface EventProxyServerOptions {
+ /** Port to start the event proxy server at. */
+ port: number;
+ /** The name for the proxy server used for referencing it with listener functions */
+ proxyServerName: string;
+}
+
+interface SentryRequestCallbackData {
+ envelope: Envelope;
+ rawProxyRequestBody: string;
+ rawSentryResponseBody: string;
+ sentryResponseStatusCode?: number;
+}
+
+/**
+ * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel`
+ * option to this server (like this `tunnel: http://localhost:${port option}/`).
+ */
+export async function startEventProxyServer(options: EventProxyServerOptions): Promise {
+ const eventCallbackListeners: Set<(data: string) => void> = new Set();
+
+ const proxyServer = http.createServer((proxyRequest, proxyResponse) => {
+ const proxyRequestChunks: Uint8Array[] = [];
+
+ proxyRequest.addListener('data', (chunk: Buffer) => {
+ proxyRequestChunks.push(chunk);
+ });
+
+ proxyRequest.addListener('error', err => {
+ throw err;
+ });
+
+ proxyRequest.addListener('end', () => {
+ const proxyRequestBody =
+ proxyRequest.headers['content-encoding'] === 'gzip'
+ ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString()
+ : Buffer.concat(proxyRequestChunks).toString();
+
+ let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]);
+
+ if (!envelopeHeader.dsn) {
+ throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.');
+ }
+
+ const { origin, pathname, host } = new URL(envelopeHeader.dsn);
+
+ const projectId = pathname.substring(1);
+ const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`;
+
+ proxyRequest.headers.host = host;
+
+ const sentryResponseChunks: Uint8Array[] = [];
+
+ const sentryRequest = https.request(
+ sentryIngestUrl,
+ { headers: proxyRequest.headers, method: proxyRequest.method },
+ sentryResponse => {
+ sentryResponse.addListener('data', (chunk: Buffer) => {
+ proxyResponse.write(chunk, 'binary');
+ sentryResponseChunks.push(chunk);
+ });
+
+ sentryResponse.addListener('end', () => {
+ eventCallbackListeners.forEach(listener => {
+ const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString();
+
+ const data: SentryRequestCallbackData = {
+ envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()),
+ rawProxyRequestBody: proxyRequestBody,
+ rawSentryResponseBody,
+ sentryResponseStatusCode: sentryResponse.statusCode,
+ };
+
+ listener(Buffer.from(JSON.stringify(data)).toString('base64'));
+ });
+ proxyResponse.end();
+ });
+
+ sentryResponse.addListener('error', err => {
+ throw err;
+ });
+
+ proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers);
+ },
+ );
+
+ sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary');
+ sentryRequest.end();
+ });
+ });
+
+ const proxyServerStartupPromise = new Promise(resolve => {
+ proxyServer.listen(options.port, () => {
+ resolve();
+ });
+ });
+
+ const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => {
+ eventCallbackResponse.statusCode = 200;
+ eventCallbackResponse.setHeader('connection', 'keep-alive');
+
+ const callbackListener = (data: string): void => {
+ eventCallbackResponse.write(data.concat('\n'), 'utf8');
+ };
+
+ eventCallbackListeners.add(callbackListener);
+
+ eventCallbackRequest.on('close', () => {
+ eventCallbackListeners.delete(callbackListener);
+ });
+
+ eventCallbackRequest.on('error', () => {
+ eventCallbackListeners.delete(callbackListener);
+ });
+ });
+
+ const eventCallbackServerStartupPromise = new Promise(resolve => {
+ eventCallbackServer.listen(0, () => {
+ const port = String((eventCallbackServer.address() as AddressInfo).port);
+ void registerCallbackServerPort(options.proxyServerName, port).then(resolve);
+ });
+ });
+
+ await eventCallbackServerStartupPromise;
+ await proxyServerStartupPromise;
+ return;
+}
+
+export async function waitForRequest(
+ proxyServerName: string,
+ callback: (eventData: SentryRequestCallbackData) => Promise | boolean,
+): Promise {
+ const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName);
+
+ return new Promise((resolve, reject) => {
+ const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
+ let eventContents = '';
+
+ response.on('error', err => {
+ reject(err);
+ });
+
+ response.on('data', (chunk: Buffer) => {
+ const chunkString = chunk.toString('utf8');
+ chunkString.split('').forEach(char => {
+ if (char === '\n') {
+ const eventCallbackData: SentryRequestCallbackData = JSON.parse(
+ Buffer.from(eventContents, 'base64').toString('utf8'),
+ );
+ const callbackResult = callback(eventCallbackData);
+ if (typeof callbackResult !== 'boolean') {
+ callbackResult.then(
+ match => {
+ if (match) {
+ response.destroy();
+ resolve(eventCallbackData);
+ }
+ },
+ err => {
+ throw err;
+ },
+ );
+ } else if (callbackResult) {
+ response.destroy();
+ resolve(eventCallbackData);
+ }
+ eventContents = '';
+ } else {
+ eventContents = eventContents.concat(char);
+ }
+ });
+ });
+ });
+
+ request.end();
+ });
+}
+
+export function waitForEnvelopeItem(
+ proxyServerName: string,
+ callback: (envelopeItem: EnvelopeItem) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForRequest(proxyServerName, async eventData => {
+ const envelopeItems = eventData.envelope[1];
+ for (const envelopeItem of envelopeItems) {
+ if (await callback(envelopeItem)) {
+ resolve(envelopeItem);
+ return true;
+ }
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+export function waitForError(
+ proxyServerName: string,
+ callback: (transactionEvent: Event) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForEnvelopeItem(proxyServerName, async envelopeItem => {
+ const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
+ if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
+ resolve(envelopeItemBody as Event);
+ return true;
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+export function waitForTransaction(
+ proxyServerName: string,
+ callback: (transactionEvent: Event) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForEnvelopeItem(proxyServerName, async envelopeItem => {
+ const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
+ if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
+ resolve(envelopeItemBody as Event);
+ return true;
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+const TEMP_FILE_PREFIX = 'event-proxy-server-';
+
+async function registerCallbackServerPort(serverName: string, port: string): Promise {
+ const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
+ await writeFile(tmpFilePath, port, { encoding: 'utf8' });
+}
+
+function retrieveCallbackServerPort(serverName: string): Promise {
+ const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
+ return readFile(tmpFilePath, 'utf8');
+}
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/package.json b/packages/e2e-tests/test-applications/node-hapi-app/package.json
new file mode 100644
index 000000000000..1f667abc8987
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "node-hapi-app",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "tsc",
+ "start": "node src/app.js",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules,pnpm-lock.yaml",
+ "test:build": "pnpm install",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@hapi/hapi": "21.3.2",
+ "@sentry/integrations": "latest || *",
+ "@sentry/node": "latest || *",
+ "@sentry/tracing": "latest || *",
+ "@sentry/types": "latest || *",
+ "@types/node": "18.15.1",
+ "typescript": "4.9.5"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.27.1",
+ "ts-node": "10.9.1"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/playwright.config.ts b/packages/e2e-tests/test-applications/node-hapi-app/playwright.config.ts
new file mode 100644
index 000000000000..1b478c6ba6da
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/playwright.config.ts
@@ -0,0 +1,77 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+import { devices } from '@playwright/test';
+
+const hapiPort = 3030;
+const eventProxyPort = 3031;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: './tests',
+ /* Maximum time one test can run for. */
+ timeout: 150_000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000,
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: 0,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'list',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: `http://localhost:${hapiPort}`,
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ // For now we only test Chrome!
+ // {
+ // name: 'firefox',
+ // use: {
+ // ...devices['Desktop Firefox'],
+ // },
+ // },
+ // {
+ // name: 'webkit',
+ // use: {
+ // ...devices['Desktop Safari'],
+ // },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: [
+ {
+ command: 'pnpm ts-node-script start-event-proxy.ts',
+ port: eventProxyPort,
+ },
+ {
+ command: 'pnpm start',
+ port: hapiPort,
+ },
+ ],
+};
+
+export default config;
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/src/app.js b/packages/e2e-tests/test-applications/node-hapi-app/src/app.js
new file mode 100644
index 000000000000..4c71802c9be2
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/src/app.js
@@ -0,0 +1,61 @@
+const Sentry = require('@sentry/node');
+const Hapi = require('@hapi/hapi');
+
+const server = Hapi.server({
+ port: 3030,
+ host: 'localhost',
+});
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.E2E_TEST_DSN,
+ includeLocalVariables: true,
+ integrations: [new Sentry.Integrations.Hapi({ server })],
+ debug: true,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1,
+});
+
+const init = async () => {
+ server.route({
+ method: 'GET',
+ path: '/test-success',
+ handler: function (request, h) {
+ return { version: 'v1' };
+ },
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/test-param/{param}',
+ handler: function (request, h) {
+ return { paramWas: request.params.param };
+ },
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/test-error',
+ handler: async function (request, h) {
+ const exceptionId = Sentry.captureException(new Error('This is an error'));
+
+ await Sentry.flush(2000);
+
+ return { exceptionId };
+ },
+ });
+
+ server.route({
+ method: 'GET',
+ path: '/test-failure',
+ handler: async function (request, h) {
+ throw new Error('This is an error');
+ },
+ });
+};
+
+(async () => {
+ init();
+ await server.start();
+ console.log('Server running on %s', server.info.uri);
+})();
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts b/packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts
new file mode 100644
index 000000000000..7a3ed463e2ae
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from './event-proxy-server';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'node-hapi-app',
+});
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts
new file mode 100644
index 000000000000..cbcd99e756d7
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts
@@ -0,0 +1,194 @@
+import { expect, test } from '@playwright/test';
+import axios, { AxiosError, AxiosResponse } from 'axios';
+import { waitForError, waitForTransaction } from '../event-proxy-server';
+
+const authToken = process.env.E2E_TEST_AUTH_TOKEN;
+const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
+const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
+const EVENT_POLLING_TIMEOUT = 90_000;
+
+test('Sends captured exception to Sentry', async ({ baseURL }) => {
+ const { data } = await axios.get(`${baseURL}/test-error`);
+ const { exceptionId } = data;
+
+ const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`;
+
+ console.log(`Polling for error eventId: ${exceptionId}`);
+
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } });
+
+ return response.status;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ { timeout: EVENT_POLLING_TIMEOUT },
+ )
+ .toBe(200);
+});
+
+test('Sends thrown error to Sentry', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('node-hapi-app', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'This is an error';
+ });
+
+ try {
+ await axios.get(`${baseURL}/test-failure`);
+ } catch (e) {}
+
+ const errorEvent = await errorEventPromise;
+ const errorEventId = errorEvent.event_id;
+
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${errorEventId}/`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return response.status;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toBe(200);
+});
+
+test('Sends successful transactions to Sentry', async ({ baseURL }) => {
+ const pageloadTransactionEventPromise = waitForTransaction('node-hapi-app', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'hapi.request' && transactionEvent?.transaction === '/test-success'
+ );
+ });
+
+ await axios.get(`${baseURL}/test-success`);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+ const transactionEventId = transactionEvent.event_id;
+
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return response.status;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toBe(200);
+});
+
+test('Sends parameterized transactions to Sentry', async ({ baseURL }) => {
+ const pageloadTransactionEventPromise = waitForTransaction('node-hapi-app', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'hapi.request' &&
+ transactionEvent?.transaction === '/test-param/{param}'
+ );
+ });
+
+ await axios.get(`${baseURL}/test-param/123`);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+ const transactionEventId = transactionEvent.event_id;
+
+ expect(transactionEvent?.contexts?.trace?.op).toBe('hapi.request');
+ expect(transactionEvent?.transaction).toBe('/test-param/{param}');
+
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return response.status;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toBe(200);
+});
+
+test('Sends sentry-trace and baggage as response headers', async ({ baseURL }) => {
+ const data = await axios.get(`${baseURL}/test-success`);
+
+ expect(data.headers).toHaveProperty('sentry-trace');
+ expect(data.headers).toHaveProperty('baggage');
+});
+
+test('Continues trace and baggage from incoming headers', async ({ baseURL }) => {
+ const traceContent = '12312012123120121231201212312012-1121201211212012-0';
+ const baggageContent = 'sentry-release=2.0.0,sentry-environment=myEnv';
+
+ await axios.get(`${baseURL}/test-success`);
+
+ const data = await axios.get(`${baseURL}/test-success`, {
+ headers: {
+ 'sentry-trace': traceContent,
+ baggage: baggageContent,
+ },
+ });
+
+ expect(data.headers).toHaveProperty('sentry-trace');
+ expect(data.headers).toHaveProperty('baggage');
+
+ expect(data.headers['sentry-trace']).toContain('12312012123120121231201212312012-');
+ expect(data.headers['baggage']).toContain(baggageContent);
+});
diff --git a/packages/e2e-tests/test-applications/node-hapi-app/tsconfig.json b/packages/e2e-tests/test-applications/node-hapi-app/tsconfig.json
new file mode 100644
index 000000000000..17bd2c1f4c00
--- /dev/null
+++ b/packages/e2e-tests/test-applications/node-hapi-app/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "types": ["node"],
+ "esModuleInterop": true,
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "strict": true,
+ "outDir": "dist"
+ },
+ "include": ["*.ts"]
+}
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/.gitignore b/packages/e2e-tests/test-applications/sveltekit-2/.gitignore
new file mode 100644
index 000000000000..6635cf554275
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/.gitignore
@@ -0,0 +1,10 @@
+.DS_Store
+node_modules
+/build
+/.svelte-kit
+/package
+.env
+.env.*
+!.env.example
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/.npmrc b/packages/e2e-tests/test-applications/sveltekit-2/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/README.md b/packages/e2e-tests/test-applications/sveltekit-2/README.md
new file mode 100644
index 000000000000..7c0d9fbb26ab
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/README.md
@@ -0,0 +1,41 @@
+# create-svelte
+
+Everything you need to build a Svelte project, powered by
+[`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
+
+## Creating a project
+
+If you're seeing this, you've probably already done this step. Congrats!
+
+```bash
+# create a new project in the current directory
+npm create svelte@latest
+
+# create a new project in my-app
+npm create svelte@latest my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a
+development server:
+
+```bash
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+To create a production version of your app:
+
+```bash
+npm run build
+```
+
+You can preview the production build with `npm run preview`.
+
+> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target
+> environment.
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts b/packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts
new file mode 100644
index 000000000000..66a9e744846e
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts
@@ -0,0 +1,253 @@
+import * as fs from 'fs';
+import * as http from 'http';
+import * as https from 'https';
+import type { AddressInfo } from 'net';
+import * as os from 'os';
+import * as path from 'path';
+import * as util from 'util';
+import * as zlib from 'zlib';
+import type { Envelope, EnvelopeItem, Event } from '@sentry/types';
+import { parseEnvelope } from '@sentry/utils';
+
+const readFile = util.promisify(fs.readFile);
+const writeFile = util.promisify(fs.writeFile);
+
+interface EventProxyServerOptions {
+ /** Port to start the event proxy server at. */
+ port: number;
+ /** The name for the proxy server used for referencing it with listener functions */
+ proxyServerName: string;
+}
+
+interface SentryRequestCallbackData {
+ envelope: Envelope;
+ rawProxyRequestBody: string;
+ rawSentryResponseBody: string;
+ sentryResponseStatusCode?: number;
+}
+
+/**
+ * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel`
+ * option to this server (like this `tunnel: http://localhost:${port option}/`).
+ */
+export async function startEventProxyServer(options: EventProxyServerOptions): Promise {
+ const eventCallbackListeners: Set<(data: string) => void> = new Set();
+
+ const proxyServer = http.createServer((proxyRequest, proxyResponse) => {
+ const proxyRequestChunks: Uint8Array[] = [];
+
+ proxyRequest.addListener('data', (chunk: Buffer) => {
+ proxyRequestChunks.push(chunk);
+ });
+
+ proxyRequest.addListener('error', err => {
+ throw err;
+ });
+
+ proxyRequest.addListener('end', () => {
+ const proxyRequestBody =
+ proxyRequest.headers['content-encoding'] === 'gzip'
+ ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString()
+ : Buffer.concat(proxyRequestChunks).toString();
+
+ let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]);
+
+ if (!envelopeHeader.dsn) {
+ throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.');
+ }
+
+ const { origin, pathname, host } = new URL(envelopeHeader.dsn);
+
+ const projectId = pathname.substring(1);
+ const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`;
+
+ proxyRequest.headers.host = host;
+
+ const sentryResponseChunks: Uint8Array[] = [];
+
+ const sentryRequest = https.request(
+ sentryIngestUrl,
+ { headers: proxyRequest.headers, method: proxyRequest.method },
+ sentryResponse => {
+ sentryResponse.addListener('data', (chunk: Buffer) => {
+ proxyResponse.write(chunk, 'binary');
+ sentryResponseChunks.push(chunk);
+ });
+
+ sentryResponse.addListener('end', () => {
+ eventCallbackListeners.forEach(listener => {
+ const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString();
+
+ const data: SentryRequestCallbackData = {
+ envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()),
+ rawProxyRequestBody: proxyRequestBody,
+ rawSentryResponseBody,
+ sentryResponseStatusCode: sentryResponse.statusCode,
+ };
+
+ listener(Buffer.from(JSON.stringify(data)).toString('base64'));
+ });
+ proxyResponse.end();
+ });
+
+ sentryResponse.addListener('error', err => {
+ throw err;
+ });
+
+ proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers);
+ },
+ );
+
+ sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary');
+ sentryRequest.end();
+ });
+ });
+
+ const proxyServerStartupPromise = new Promise(resolve => {
+ proxyServer.listen(options.port, () => {
+ resolve();
+ });
+ });
+
+ const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => {
+ eventCallbackResponse.statusCode = 200;
+ eventCallbackResponse.setHeader('connection', 'keep-alive');
+
+ const callbackListener = (data: string): void => {
+ eventCallbackResponse.write(data.concat('\n'), 'utf8');
+ };
+
+ eventCallbackListeners.add(callbackListener);
+
+ eventCallbackRequest.on('close', () => {
+ eventCallbackListeners.delete(callbackListener);
+ });
+
+ eventCallbackRequest.on('error', () => {
+ eventCallbackListeners.delete(callbackListener);
+ });
+ });
+
+ const eventCallbackServerStartupPromise = new Promise(resolve => {
+ eventCallbackServer.listen(0, () => {
+ const port = String((eventCallbackServer.address() as AddressInfo).port);
+ void registerCallbackServerPort(options.proxyServerName, port).then(resolve);
+ });
+ });
+
+ await eventCallbackServerStartupPromise;
+ await proxyServerStartupPromise;
+ return;
+}
+
+export async function waitForRequest(
+ proxyServerName: string,
+ callback: (eventData: SentryRequestCallbackData) => Promise | boolean,
+): Promise {
+ const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName);
+
+ return new Promise((resolve, reject) => {
+ const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => {
+ let eventContents = '';
+
+ response.on('error', err => {
+ reject(err);
+ });
+
+ response.on('data', (chunk: Buffer) => {
+ const chunkString = chunk.toString('utf8');
+ chunkString.split('').forEach(char => {
+ if (char === '\n') {
+ const eventCallbackData: SentryRequestCallbackData = JSON.parse(
+ Buffer.from(eventContents, 'base64').toString('utf8'),
+ );
+ const callbackResult = callback(eventCallbackData);
+ if (typeof callbackResult !== 'boolean') {
+ callbackResult.then(
+ match => {
+ if (match) {
+ response.destroy();
+ resolve(eventCallbackData);
+ }
+ },
+ err => {
+ throw err;
+ },
+ );
+ } else if (callbackResult) {
+ response.destroy();
+ resolve(eventCallbackData);
+ }
+ eventContents = '';
+ } else {
+ eventContents = eventContents.concat(char);
+ }
+ });
+ });
+ });
+
+ request.end();
+ });
+}
+
+export function waitForEnvelopeItem(
+ proxyServerName: string,
+ callback: (envelopeItem: EnvelopeItem) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForRequest(proxyServerName, async eventData => {
+ const envelopeItems = eventData.envelope[1];
+ for (const envelopeItem of envelopeItems) {
+ if (await callback(envelopeItem)) {
+ resolve(envelopeItem);
+ return true;
+ }
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+export function waitForError(
+ proxyServerName: string,
+ callback: (transactionEvent: Event) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForEnvelopeItem(proxyServerName, async envelopeItem => {
+ const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
+ if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) {
+ resolve(envelopeItemBody as Event);
+ return true;
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+export function waitForTransaction(
+ proxyServerName: string,
+ callback: (transactionEvent: Event) => Promise | boolean,
+): Promise {
+ return new Promise((resolve, reject) => {
+ waitForEnvelopeItem(proxyServerName, async envelopeItem => {
+ const [envelopeItemHeader, envelopeItemBody] = envelopeItem;
+ if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) {
+ resolve(envelopeItemBody as Event);
+ return true;
+ }
+ return false;
+ }).catch(reject);
+ });
+}
+
+const TEMP_FILE_PREFIX = 'event-proxy-server-';
+
+async function registerCallbackServerPort(serverName: string, port: string): Promise {
+ const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
+ await writeFile(tmpFilePath, port, { encoding: 'utf8' });
+}
+
+async function retrieveCallbackServerPort(serverName: string): Promise {
+ const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`);
+ return await readFile(tmpFilePath, 'utf8');
+}
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/package.json b/packages/e2e-tests/test-applications/sveltekit-2/package.json
new file mode 100644
index 000000000000..b55d9ff74df6
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "sveltekit-2.0",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "clean": "npx rimraf node_modules,pnpm-lock.yaml",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:dev": "TEST_ENV=development playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm -v"
+ },
+ "dependencies": {
+ "@sentry/sveltekit": "latest || *"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.27.1",
+ "@sentry/types": "latest || *",
+ "@sentry/utils": "latest || *",
+ "@sveltejs/adapter-auto": "^3.0.0",
+ "@sveltejs/adapter-node": "^2.0.0",
+ "@sveltejs/kit": "^2.0.0",
+ "@sveltejs/vite-plugin-svelte": "^3.0.0",
+ "svelte": "^4.2.8",
+ "svelte-check": "^3.6.0",
+ "ts-node": "10.9.1",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.3",
+ "wait-port": "1.0.4"
+ },
+ "pnpm": {
+ "overrides": {
+ "@sentry/node": "latest || *",
+ "@sentry/tracing": "latest || *"
+ }
+ },
+ "type": "module"
+}
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts b/packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts
new file mode 100644
index 000000000000..bfa29df7d549
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts
@@ -0,0 +1,71 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+import { devices } from '@playwright/test';
+
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const port = 3030;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: './test',
+ /* Maximum time one test can run for. */
+ timeout: 150_000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 10000,
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* `next dev` is incredibly buggy with the app dir */
+ retries: testEnv === 'development' ? 3 : 0,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'list',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: `http://localhost:${port}`,
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: [
+ {
+ command: 'pnpm ts-node --esm start-event-proxy.ts',
+ port: 3031,
+ },
+ {
+ command:
+ testEnv === 'development'
+ ? `pnpm wait-port ${port} && pnpm dev --port ${port}`
+ : `pnpm wait-port ${port} && pnpm preview --port ${port}`,
+ port,
+ },
+ ],
+};
+
+export default config;
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/app.html b/packages/e2e-tests/test-applications/sveltekit-2/src/app.html
new file mode 100644
index 000000000000..117bd026151a
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.client.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.client.ts
new file mode 100644
index 000000000000..bfe90b150886
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.client.ts
@@ -0,0 +1,16 @@
+import { env } from '$env/dynamic/public';
+import * as Sentry from '@sentry/sveltekit';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: env.PUBLIC_E2E_TEST_DSN,
+ debug: true,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+});
+
+const myErrorHandler = ({ error, event }: any) => {
+ console.error('An error occurred on the client side:', error, event);
+};
+
+export const handleError = Sentry.handleErrorWithSentry(myErrorHandler);
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts
new file mode 100644
index 000000000000..ae99e0e0e7b4
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts
@@ -0,0 +1,18 @@
+import { E2E_TEST_DSN } from '$env/static/private';
+import * as Sentry from '@sentry/sveltekit';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: E2E_TEST_DSN,
+ debug: true,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+});
+
+const myErrorHandler = ({ error, event }: any) => {
+ console.error('An error occurred on the server side:', error, event);
+};
+
+export const handleError = Sentry.handleErrorWithSentry(myErrorHandler);
+
+export const handle = Sentry.sentryHandle();
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte
new file mode 100644
index 000000000000..5982b0ae37dd
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte
@@ -0,0 +1,2 @@
+Welcome to SvelteKit
+Visit kit.svelte.dev to read the documentation
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.server.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.server.ts
new file mode 100644
index 000000000000..b07376ba97c9
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.server.ts
@@ -0,0 +1,5 @@
+import type { PageServerLoad } from './$types';
+
+export const load = (async _event => {
+ return { name: 'building (server)' };
+}) satisfies PageServerLoad;
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte
new file mode 100644
index 000000000000..fde274c60705
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte
@@ -0,0 +1,6 @@
+Check Build
+
+
+ This route only exists to check that Typescript definitions
+ and auto instrumentation are working when the project is built.
+
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.ts
new file mode 100644
index 000000000000..049acdc1fafa
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.ts
@@ -0,0 +1,5 @@
+import type { PageLoad } from './$types';
+
+export const load = (async _event => {
+ return { name: 'building' };
+}) satisfies PageLoad;
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts b/packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts
new file mode 100644
index 000000000000..3af64eb5960a
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from './event-proxy-server';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'sveltekit-2',
+});
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/static/favicon.png b/packages/e2e-tests/test-applications/sveltekit-2/static/favicon.png
new file mode 100644
index 000000000000..825b9e65af7c
Binary files /dev/null and b/packages/e2e-tests/test-applications/sveltekit-2/static/favicon.png differ
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/svelte.config.js b/packages/e2e-tests/test-applications/sveltekit-2/svelte.config.js
new file mode 100644
index 000000000000..c521eff7de30
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/svelte.config.js
@@ -0,0 +1,18 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://kit.svelte.dev/docs/integrations#preprocessors
+ // for more information about preprocessors
+ preprocess: vitePreprocess(),
+
+ kit: {
+ // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
+ // If your environment is not supported or you settled on a specific environment, switch out the adapter.
+ // See https://kit.svelte.dev/docs/adapters for more information about adapters.
+ adapter: adapter(),
+ },
+};
+
+export default config;
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts b/packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts
new file mode 100644
index 000000000000..7d621af34dcf
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts
@@ -0,0 +1,48 @@
+import { expect, test } from '@playwright/test';
+import axios, { AxiosError } from 'axios';
+// @ts-expect-error ok ok
+import { waitForTransaction } from '../event-proxy-server.ts';
+
+const authToken = process.env.E2E_TEST_AUTH_TOKEN;
+const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
+const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
+const EVENT_POLLING_TIMEOUT = 90_000;
+
+test('Sends a pageload transaction', async ({ page }) => {
+ const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => {
+ return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/';
+ });
+
+ await page.goto('/');
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+ const transactionEventId = transactionEvent.event_id;
+
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return response.status;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toBe(200);
+});
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/tsconfig.json b/packages/e2e-tests/test-applications/sveltekit-2/tsconfig.json
new file mode 100644
index 000000000000..12aa7328fc83
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "allowImportingTsExtensions": true
+ },
+ "ts-node": {
+ "esm": true,
+ "experimentalSpecifierResolution": "node"
+ }
+ // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
+ //
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+ // from the referenced tsconfig.json - TypeScript does not merge them in
+}
diff --git a/packages/e2e-tests/test-applications/sveltekit-2/vite.config.js b/packages/e2e-tests/test-applications/sveltekit-2/vite.config.js
new file mode 100644
index 000000000000..1a410bee7e11
--- /dev/null
+++ b/packages/e2e-tests/test-applications/sveltekit-2/vite.config.js
@@ -0,0 +1,12 @@
+import { sentrySvelteKit } from '@sentry/sveltekit';
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ sentrySvelteKit({
+ autoUploadSourceMaps: false,
+ }),
+ sveltekit(),
+ ],
+});
diff --git a/packages/feedback/README.md b/packages/feedback/README.md
index 7aa8df72cd80..673bf344ea75 100644
--- a/packages/feedback/README.md
+++ b/packages/feedback/README.md
@@ -73,16 +73,15 @@ By default the Feedback integration will attempt to fill in the name/email field
```javascript
Sentry.setUser({
- email: 'foo@example.com',
+ userEmail: 'foo@example.com',
fullName: 'Jane Doe',
});
-
new Feedback({
- useSentryUser({
- email: 'email',
- name: 'fullName',
- }),
+ useSentryUser: {
+ email: 'userEmail',
+ name: 'fullName',
+ },
})
```
diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts
index b8ec16a15401..f1629a00670a 100644
--- a/packages/feedback/src/util/sendFeedbackRequest.ts
+++ b/packages/feedback/src/util/sendFeedbackRequest.ts
@@ -1,4 +1,4 @@
-import { createEventEnvelope, getCurrentHub } from '@sentry/core';
+import { createEventEnvelope, getClient, withScope } from '@sentry/core';
import type { FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types';
import { FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE } from '../constants';
@@ -12,8 +12,7 @@ export async function sendFeedbackRequest(
{ feedback: { message, email, name, source, url } }: SendFeedbackData,
{ includeReplay = true }: SendFeedbackOptions = {},
): Promise {
- const hub = getCurrentHub();
- const client = hub.getClient();
+ const client = getClient();
const transport = client && client.getTransport();
const dsn = client && client.getDsn();
@@ -34,67 +33,58 @@ export async function sendFeedbackRequest(
type: 'feedback',
};
- return new Promise((resolve, reject) => {
- hub.withScope(async scope => {
- // No use for breadcrumbs in feedback
- scope.clearBreadcrumbs();
+ return withScope(async scope => {
+ // No use for breadcrumbs in feedback
+ scope.clearBreadcrumbs();
- if ([FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE].includes(String(source))) {
- scope.setLevel('info');
- }
+ if ([FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE].includes(String(source))) {
+ scope.setLevel('info');
+ }
- const feedbackEvent = await prepareFeedbackEvent({
- scope,
- client,
- event: baseEvent,
- });
+ const feedbackEvent = await prepareFeedbackEvent({
+ scope,
+ client,
+ event: baseEvent,
+ });
- if (feedbackEvent === null) {
- resolve();
- return;
- }
+ if (!feedbackEvent) {
+ return;
+ }
- if (client && client.emit) {
- client.emit('beforeSendFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) });
- }
+ if (client.emit) {
+ client.emit('beforeSendFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) });
+ }
- const envelope = createEventEnvelope(
- feedbackEvent,
- dsn,
- client.getOptions()._metadata,
- client.getOptions().tunnel,
- );
+ const envelope = createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel);
- let response: void | TransportMakeRequestResponse;
+ let response: void | TransportMakeRequestResponse;
+
+ try {
+ response = await transport.send(envelope);
+ } catch (err) {
+ const error = new Error('Unable to send Feedback');
try {
- response = await transport.send(envelope);
- } catch (err) {
- const error = new Error('Unable to send Feedback');
-
- try {
- // In case browsers don't allow this property to be writable
- // @ts-expect-error This needs lib es2022 and newer
- error.cause = err;
- } catch {
- // nothing to do
- }
- reject(error);
+ // In case browsers don't allow this property to be writable
+ // @ts-expect-error This needs lib es2022 and newer
+ error.cause = err;
+ } catch {
+ // nothing to do
}
+ throw error;
+ }
- // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore
- if (!response) {
- resolve(response);
- return;
- }
+ // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore
+ if (!response) {
+ return;
+ }
- // Require valid status codes, otherwise can assume feedback was not sent successfully
- if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
- reject(new Error('Unable to send Feedback'));
- }
+ // Require valid status codes, otherwise can assume feedback was not sent successfully
+ if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
+ throw new Error('Unable to send Feedback');
+ }
- resolve(response);
- });
+ return response;
});
}
diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts
index 35f9fcf51f71..b5e414803121 100644
--- a/packages/feedback/src/widget/createWidget.ts
+++ b/packages/feedback/src/widget/createWidget.ts
@@ -1,4 +1,4 @@
-import { getCurrentHub } from '@sentry/core';
+import { getCurrentScope } from '@sentry/core';
import { logger } from '@sentry/utils';
import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget } from '../types';
@@ -160,7 +160,7 @@ export function createWidget({
}
const userKey = options.useSentryUser;
- const scope = getCurrentHub().getScope();
+ const scope = getCurrentScope();
const user = scope && scope.getUser();
dialog = Dialog({
diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts
index 7797b1d0e7d5..057d0e6a9975 100644
--- a/packages/hub/src/index.ts
+++ b/packages/hub/src/index.ts
@@ -112,6 +112,7 @@ export const captureMessage = captureMessageCore;
/**
* @deprecated This export has moved to @sentry/core. The @sentry/hub package will be removed in v8.
*/
+// eslint-disable-next-line deprecation/deprecation
export const configureScope = configureScopeCore;
/**
diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts
index 1e1ee0318861..c03cd63e6840 100644
--- a/packages/integrations/src/httpclient.ts
+++ b/packages/integrations/src/httpclient.ts
@@ -1,4 +1,4 @@
-import { getCurrentHub, isSentryRequestUrl } from '@sentry/core';
+import { getClient, isSentryRequestUrl } from '@sentry/core';
import type {
Event as SentryEvent,
EventProcessor,
@@ -348,9 +348,7 @@ export class HttpClient implements Integration {
*/
private _shouldCaptureResponse(status: number, url: string): boolean {
return (
- this._isInGivenStatusRanges(status) &&
- this._isInGivenRequestTargets(url) &&
- !isSentryRequestUrl(url, getCurrentHub())
+ this._isInGivenStatusRanges(status) && this._isInGivenRequestTargets(url) && !isSentryRequestUrl(url, getClient())
);
}
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index 8fd55568e70e..0c10a8344bd7 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -4,8 +4,8 @@ import type { BrowserOptions } from '@sentry/react';
import {
BrowserTracing,
Integrations,
- configureScope,
defaultRequestInstrumentationOptions,
+ getCurrentScope,
init as reactInit,
} from '@sentry/react';
import type { EventProcessor } from '@sentry/types';
@@ -56,17 +56,16 @@ export function init(options: BrowserOptions): void {
reactInit(opts);
- configureScope(scope => {
- scope.setTag('runtime', 'browser');
- const filterTransactions: EventProcessor = event =>
- event.type === 'transaction' && event.transaction === '/404' ? null : event;
- filterTransactions.id = 'NextClient404Filter';
- scope.addEventProcessor(filterTransactions);
+ const scope = getCurrentScope();
+ scope.setTag('runtime', 'browser');
+ const filterTransactions: EventProcessor = event =>
+ event.type === 'transaction' && event.transaction === '/404' ? null : event;
+ filterTransactions.id = 'NextClient404Filter';
+ scope.addEventProcessor(filterTransactions);
- if (process.env.NODE_ENV === 'development') {
- scope.addEventProcessor(devErrorSymbolicationEventProcessor);
- }
- });
+ if (process.env.NODE_ENV === 'development') {
+ scope.addEventProcessor(devErrorSymbolicationEventProcessor);
+ }
}
function addClientIntegrations(options: BrowserOptions): void {
diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts
index 91929f885ae0..a7c3d5bd2344 100644
--- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts
+++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts
@@ -1,5 +1,5 @@
import type { ParsedUrlQuery } from 'querystring';
-import { getClient, getCurrentHub } from '@sentry/core';
+import { getClient, getCurrentScope } from '@sentry/core';
import { WINDOW } from '@sentry/react';
import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types';
import {
@@ -124,7 +124,7 @@ export function pagesRouterInstrumentation(
baggage,
);
- getCurrentHub().getScope().setPropagationContext(propagationContext);
+ getCurrentScope().setPropagationContext(propagationContext);
prevLocationName = route || globalObject.location.pathname;
if (startTransactionOnPageLoad) {
diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts
index 063c11bff62a..3b0ce67fb16c 100644
--- a/packages/nextjs/src/common/index.ts
+++ b/packages/nextjs/src/common/index.ts
@@ -44,4 +44,6 @@ export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry';
export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry';
+export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry';
+
export { withServerActionInstrumentation } from './withServerActionInstrumentation';
diff --git a/packages/nextjs/src/common/types.ts b/packages/nextjs/src/common/types.ts
index ffca3dc8ff61..cf7d881e9ea0 100644
--- a/packages/nextjs/src/common/types.ts
+++ b/packages/nextjs/src/common/types.ts
@@ -1,5 +1,6 @@
import type { Transaction, WebFetchHeaders, WrappedFunction } from '@sentry/types';
import type { NextApiRequest, NextApiResponse } from 'next';
+import type { RequestAsyncStorage } from '../config/templates/requestAsyncStorageShim';
export type ServerComponentContext = {
componentRoute: string;
@@ -17,6 +18,13 @@ export type ServerComponentContext = {
headers?: WebFetchHeaders;
};
+export type GenerationFunctionContext = {
+ requestAsyncStorage?: RequestAsyncStorage;
+ componentRoute: string;
+ componentType: string;
+ generationFunctionIdentifier: string;
+};
+
export interface RouteHandlerContext {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
parameterizedRoute: string;
diff --git a/packages/nextjs/src/common/utils/commonObjectTracing.ts b/packages/nextjs/src/common/utils/commonObjectTracing.ts
new file mode 100644
index 000000000000..bb5cf130bab1
--- /dev/null
+++ b/packages/nextjs/src/common/utils/commonObjectTracing.ts
@@ -0,0 +1,23 @@
+import type { PropagationContext } from '@sentry/types';
+
+const commonMap = new WeakMap