');
@@ -930,7 +929,7 @@ describe('soft navigation web vitals', () => {
expect(SentryCoreBrowser.startInactiveSpan).toHaveBeenCalledWith(
expect.objectContaining({
parentSpan: bfcacheNavigationSpan,
- attributes: expect.objectContaining({ 'browser.navigation.type': 'bfcache' }),
+ attributes: expect.objectContaining({ 'browser.navigation.type': 'back-forward-cache' }),
}),
);
expect(SentryCoreBrowser.startInactiveSpan).not.toHaveBeenCalledWith(
@@ -961,7 +960,7 @@ describe('soft navigation web vitals', () => {
});
it("does not let a restore's own vital span become the parent of the next one", () => {
- // Web vital spans for a restore carry the same `bfcache` navigation type as the navigation span
+ // Web vital spans for a restore carry the same `back-forward-cache` navigation type as the navigation span
// they hang off, so the second vital would otherwise be parented to the first.
vi.mocked(SentryCore.getActiveSpan).mockReturnValue(undefined);
diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts
index a6d9d57766ee..6708e12549ba 100644
--- a/packages/browser/src/exports.ts
+++ b/packages/browser/src/exports.ts
@@ -81,7 +81,6 @@ export {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
- SENTRY_SEGMENT_NAME_SOURCE,
} from '@sentry/core';
export { WINDOW } from './helpers';
diff --git a/packages/browser/src/integrations/httpclient.ts b/packages/browser/src/integrations/httpclient.ts
index f9d01c5719b5..a47725b3d37d 100644
--- a/packages/browser/src/integrations/httpclient.ts
+++ b/packages/browser/src/integrations/httpclient.ts
@@ -93,16 +93,12 @@ function _fetchResponseHandler(
const reqCookieStr = request.headers.get('Cookie') || undefined;
if (reqCookieStr) {
const filtered = _INTERNAL_filterCookies(reqCookieStr, dc.cookies);
- if (typeof filtered === 'object') {
- requestCookies = filtered;
- }
+ requestCookies = typeof filtered === 'string' ? { cookie: filtered } : filtered;
}
const resCookieStr = response.headers.get('Set-Cookie') || undefined;
if (resCookieStr) {
const filtered = _INTERNAL_filterCookies(resCookieStr, dc.cookies);
- if (typeof filtered === 'object') {
- responseCookies = filtered;
- }
+ responseCookies = typeof filtered === 'string' ? { 'set-cookie': filtered } : filtered;
}
}
@@ -146,9 +142,7 @@ function _xhrResponseHandler(
const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined;
if (cookieString) {
const filtered = _INTERNAL_filterCookies(cookieString, dc.cookies);
- if (typeof filtered === 'object') {
- responseCookies = filtered;
- }
+ responseCookies = typeof filtered === 'string' ? { 'set-cookie': filtered } : filtered;
}
} catch {
// ignore it if parsing fails
diff --git a/packages/browser/src/integrations/webVitals.ts b/packages/browser/src/integrations/webVitals.ts
index 7c1b3d14a9fe..3d62d9146a53 100644
--- a/packages/browser/src/integrations/webVitals.ts
+++ b/packages/browser/src/integrations/webVitals.ts
@@ -49,7 +49,7 @@ export interface WebVitalsOptions {
*
* A restore is a new page view measured against a document that was never reloaded, so its vitals
* are reported against the navigation span `browserTracingIntegration` starts for the restore,
- * and tagged `browser.navigation.type: bfcache`. A restore is near-instant by construction, so
+ * and tagged `browser.navigation.type: back-forward-cache`. A restore is near-instant by construction, so
* these are a distinct population from page load vitals and are meant to be read through that
* attribute rather than pooled with them. Set this to `false` to leave restores unmeasured.
*
diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
index 080d4154f609..b791dddecefc 100644
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -723,7 +723,7 @@ export const browserTracingIntegration = ((options: Partial
{
expect.objectContaining({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache',
- 'browser.navigation.type': 'bfcache',
+ 'browser.navigation.type': 'back-forward-cache',
}),
);
});
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index c9f7981a3122..10aa2d422844 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -83,6 +83,8 @@ export {
onUnhandledRejectionIntegration,
mistralAIIntegration,
openAIIntegration,
+ groqIntegration,
+ togetherAIIntegration,
langChainIntegration,
langGraphIntegration,
mastraIntegration,
@@ -114,7 +116,6 @@ export {
parameterize,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SENTRY_SEGMENT_NAME_SOURCE,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
dataloaderIntegration,
expressIntegration,
@@ -191,6 +192,8 @@ export {
// oxlint-disable-next-line typescript/no-deprecated
withStreamedSpan,
eveConversationHook,
+ eveInstrumentation,
+ eveIntegration,
getInstrumentedModuleNames,
} from '@sentry/node';
@@ -215,5 +218,6 @@ export { bunServerIntegration } from './integrations/bunserver';
export type { BunServerIntegrationOptions } from './integrations/bunserver';
export { bunHttpServerIntegration } from './integrations/bunHttpServer';
export { fetchIntegration } from './integrations/fetch';
+export type { FetchIntegrationOptions } from '@sentry/core';
export { bunRuntimeMetricsIntegration, type BunRuntimeMetricsOptions } from './integrations/bunRuntimeMetrics';
export { makeFetchTransport } from './transports';
diff --git a/packages/bun/src/integrations/fetch.ts b/packages/bun/src/integrations/fetch.ts
index b908ccaf2e25..21e01ccb2307 100644
--- a/packages/bun/src/integrations/fetch.ts
+++ b/packages/bun/src/integrations/fetch.ts
@@ -1,166 +1,10 @@
-import type {
- Client,
- FetchBreadcrumbData,
- FetchBreadcrumbHint,
- HandlerDataFetch,
- IntegrationFn,
- Span,
-} from '@sentry/core';
-import {
- addBreadcrumb,
- addFetchInstrumentationHandler,
- defineIntegration,
- getBreadcrumbLogLevelFromHttpStatusCode,
- getClient,
- instrumentFetchRequest,
- isSentryRequestUrl,
- LRUMap,
- shouldPropagateTraceForUrl,
-} from '@sentry/core';
-
-const INTEGRATION_NAME = 'Fetch' as const;
-
-const HAS_CLIENT_MAP = new WeakMap();
-
-interface FetchOptions {
- /**
- * Whether breadcrumbs should be recorded for requests.
- * Defaults to true.
- */
- breadcrumbs?: boolean;
-
- /**
- * Function determining whether or not to create spans to track outgoing requests to the given URL.
- * By default, spans will be created for all outgoing requests.
- */
- shouldCreateSpanForRequest?: (url: string) => boolean;
-}
-
-const _fetchIntegration = ((options: FetchOptions = {}) => {
- const breadcrumbs = options.breadcrumbs === undefined ? true : options.breadcrumbs;
- const shouldCreateSpanForRequest = options.shouldCreateSpanForRequest;
-
- const _createSpanUrlMap = new LRUMap(100);
- const _headersUrlMap = new LRUMap(100);
-
- const spans: Record = {};
-
- /** Decides whether to attach trace data to the outgoing fetch request */
- function _shouldAttachTraceData(url: string): boolean {
- const client = getClient();
-
- if (!client) {
- return false;
- }
-
- return shouldPropagateTraceForUrl(url, client.getOptions().tracePropagationTargets, _headersUrlMap);
- }
-
- /** Helper that wraps shouldCreateSpanForRequest option */
- function _shouldCreateSpan(url: string): boolean {
- if (shouldCreateSpanForRequest === undefined) {
- return true;
- }
-
- const cachedDecision = _createSpanUrlMap.get(url);
- if (cachedDecision !== undefined) {
- return cachedDecision;
- }
-
- const decision = shouldCreateSpanForRequest(url);
- _createSpanUrlMap.set(url, decision);
- return decision;
- }
-
- return {
- name: INTEGRATION_NAME,
- setupOnce() {
- addFetchInstrumentationHandler(handlerData => {
- const client = getClient();
- if (!client || !HAS_CLIENT_MAP.get(client)) {
- return;
- }
- const { propagateTraceparent } = client.getOptions();
-
- if (isSentryRequestUrl(handlerData.fetchData.url, client)) {
- return;
- }
-
- instrumentFetchRequest(handlerData, _shouldCreateSpan, _shouldAttachTraceData, spans, {
- spanOrigin: 'auto.http.fetch',
- propagateTraceparent,
- });
-
- if (breadcrumbs) {
- createBreadcrumb(handlerData);
- }
- });
- },
- setup(client) {
- HAS_CLIENT_MAP.set(client, true);
- },
- };
-}) satisfies IntegrationFn;
+import { createFetchIntegration } from '@sentry/core';
/**
* Instruments outgoing `fetch` requests in Bun: creates spans, records breadcrumbs and
* attaches trace propagation headers.
*/
-export const fetchIntegration = defineIntegration(_fetchIntegration);
-
-function createBreadcrumb(handlerData: HandlerDataFetch): void {
- const { startTimestamp, endTimestamp } = handlerData;
-
- // We only capture complete fetch requests
- if (!endTimestamp) {
- return;
- }
-
- const breadcrumbData: FetchBreadcrumbData = {
- method: handlerData.fetchData.method,
- url: handlerData.fetchData.url,
- };
-
- if (handlerData.error) {
- const hint: FetchBreadcrumbHint = {
- data: handlerData.error,
- input: handlerData.args,
- startTimestamp,
- endTimestamp,
- };
-
- addBreadcrumb(
- {
- category: 'fetch',
- data: breadcrumbData,
- level: 'error',
- type: 'http',
- },
- hint,
- );
- } else {
- const response = handlerData.response as Response | undefined;
-
- breadcrumbData.request_body_size = handlerData.fetchData.request_body_size;
- breadcrumbData.response_body_size = handlerData.fetchData.response_body_size;
- breadcrumbData.status_code = response?.status;
-
- const hint: FetchBreadcrumbHint = {
- input: handlerData.args,
- response,
- startTimestamp,
- endTimestamp,
- };
- const level = getBreadcrumbLogLevelFromHttpStatusCode(breadcrumbData.status_code);
-
- addBreadcrumb(
- {
- category: 'fetch',
- data: breadcrumbData,
- type: 'http',
- level,
- },
- hint,
- );
- }
-}
+export const fetchIntegration = createFetchIntegration({
+ name: 'Fetch',
+ spanOrigin: 'auto.http.fetch',
+});
diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts
index ba77f87ef9a9..983d45cb3938 100644
--- a/packages/bun/src/sdk.ts
+++ b/packages/bun/src/sdk.ts
@@ -28,12 +28,9 @@ import { bunHttpServerIntegration } from './integrations/bunHttpServer';
import { getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils';
/**
- * The performance integrations for bun: the OTel auto-performance set, but with
- * the orchestrion diagnostics-channel subscribers swapped in for their OTel
- * equivalents *only* when the orchestrion channels were actually injected (i.e.
- * the app was built with `@sentry/bun/plugin`). Without that, the channels
- * never fire — and the OTel versions rely on a runtime require-hook bun doesn't
- * support — so leave the auto-performance set alone.
+ * The tracing integrations for bun, added whenever spans are enabled. Most of them listen on
+ * the orchestrion diagnostics channels, which only exist when the app is built with
+ * `@sentry/bun/plugin`. Without the plugin, those integrations stay installed but create no spans.
*/
function getPerformanceIntegrations(options: Options): Integration[] {
if (!hasSpansEnabled(options)) {
@@ -145,7 +142,7 @@ function _init(
const options = {
...userOptions,
platform: 'javascript',
- runtime: { name: 'bun', version: typeof Bun !== 'undefined' ? Bun.version : 'unknown' },
+ runtime: userOptions.runtime || { name: 'bun', version: typeof Bun !== 'undefined' ? Bun.version : 'unknown' },
serverName: userOptions.serverName || global.process.env.SENTRY_NAME || os.hostname(),
};
diff --git a/packages/bun/src/types.ts b/packages/bun/src/types.ts
index 34643a995ab1..ade64f83137f 100644
--- a/packages/bun/src/types.ts
+++ b/packages/bun/src/types.ts
@@ -21,6 +21,14 @@ export interface BaseBunOptions extends ServerRuntimeOptions {
* @default false
*/
enableOpenTelemetrySetup?: boolean;
+
+ /**
+ * Override the runtime name reported in events.
+ * Defaults to 'bun' with the current Bun version if not specified.
+ *
+ * @hidden This is primarily used internally to support SDKs wrapping the Bun SDK, like Elysia.
+ */
+ runtime?: { name: string; version?: string };
}
/**
diff --git a/packages/bun/test/init.test.ts b/packages/bun/test/init.test.ts
index abf3aabf060e..2fced80fd67a 100644
--- a/packages/bun/test/init.test.ts
+++ b/packages/bun/test/init.test.ts
@@ -129,6 +129,20 @@ describe('init()', () => {
});
});
+ describe('runtime', () => {
+ it('defaults to bun', () => {
+ init({ dsn: PUBLIC_DSN, traceLifecycle: 'static' });
+
+ expect(getClient()?.getOptions().runtime).toEqual({ name: 'bun', version: Bun.version });
+ });
+
+ it('respects a runtime provided through options', () => {
+ init({ dsn: PUBLIC_DSN, traceLifecycle: 'static', runtime: { name: 'node', version: '20.0.0' } });
+
+ expect(getClient()?.getOptions().runtime).toEqual({ name: 'node', version: '20.0.0' });
+ });
+ });
+
describe('initWithoutDefaultIntegrations()', () => {
it('installs no default integrations', () => {
initWithoutDefaultIntegrations({ dsn: PUBLIC_DSN, traceLifecycle: 'static' });
diff --git a/packages/bun/test/integrations/fetch.test.ts b/packages/bun/test/integrations/fetch.test.ts
new file mode 100644
index 000000000000..a201d7462c89
--- /dev/null
+++ b/packages/bun/test/integrations/fetch.test.ts
@@ -0,0 +1,100 @@
+import http from 'node:http';
+import type { TransactionEvent } from '@sentry/core';
+import { getCurrentScope, getIsolationScope, startSpan } from '@sentry/core';
+import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
+import { init } from '../../src';
+
+async function startServer(
+ handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
+): Promise<{ port: number; close: () => Promise }> {
+ const server = http.createServer(handler);
+ const port = await new Promise(resolve => {
+ server.listen(0, () => resolve((server.address() as { port: number }).port));
+ });
+ return {
+ port,
+ close: () => new Promise(resolve => server.close(() => resolve())),
+ };
+}
+
+const transactions: TransactionEvent[] = [];
+
+/** Bind on the real completion signal so a "never arrives" regression fails instead of hanging. */
+function waitForTransaction(name: string): Promise {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => reject(new Error(`Timed out waiting for the "${name}" transaction`)), 5000);
+ const poll = setInterval(() => {
+ const found = transactions.find(event => event.transaction === name);
+ if (found) {
+ clearTimeout(timer);
+ clearInterval(poll);
+ resolve(found);
+ }
+ }, 10);
+ });
+}
+
+function header(headers: http.IncomingHttpHeaders | undefined, name: string): string | undefined {
+ const value = headers?.[name];
+ return Array.isArray(value) ? value[0] : value;
+}
+
+describe('fetchIntegration', () => {
+ beforeAll(() => {
+ init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ tracesSampleRate: 1.0,
+ traceLifecycle: 'static',
+ beforeSendTransaction(event) {
+ transactions.push(event);
+ return null;
+ },
+ transport: () => ({ send: async () => ({}), flush: async () => true }),
+ });
+ });
+
+ afterAll(() => {
+ getCurrentScope().setClient(undefined);
+ });
+
+ test('creates an http.client span and propagates trace headers', async () => {
+ let received: http.IncomingHttpHeaders | undefined;
+ const { port, close } = await startServer((req, res) => {
+ received = req.headers;
+ res.end('ok');
+ });
+
+ await startSpan({ name: 'parent', op: 'test' }, async () => {
+ await fetch(`http://localhost:${port}/downstream`).then(res => res.text());
+ });
+
+ const parent = await waitForTransaction('parent');
+ await close();
+
+ const clientSpan = parent.spans?.find(span => span.op === 'http.client');
+ expect(clientSpan).toBeDefined();
+ expect(clientSpan?.origin).toBe('auto.http.fetch');
+
+ const traceId = parent.contexts?.trace?.trace_id;
+ const sentryTrace = header(received, 'sentry-trace');
+ expect(sentryTrace).toBeDefined();
+ expect(sentryTrace!.split('-')[0]).toBe(traceId!);
+ expect(sentryTrace!.split('-')[1]).toBe(clientSpan!.span_id!);
+ expect(header(received, 'baggage')).toContain(`sentry-trace_id=${traceId}`);
+ });
+
+ test('records exactly one fetch breadcrumb', async () => {
+ const { port, close } = await startServer((_req, res) => res.end('ok'));
+ const url = `http://localhost:${port}/crumb`;
+
+ getIsolationScope().clearBreadcrumbs();
+ await fetch(url).then(res => res.text());
+ await close();
+
+ const crumbs = getIsolationScope()
+ .getScopeData()
+ .breadcrumbs.filter(crumb => crumb.category === 'fetch' && crumb.data?.url === url);
+
+ expect(crumbs).toHaveLength(1);
+ });
+});
diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json
index f9a7a9e6ca62..11a34f9e063a 100644
--- a/packages/bundler-plugins/package.json
+++ b/packages/bundler-plugins/package.json
@@ -24,11 +24,6 @@
"import": "./build/esm/webpack/index.js",
"require": "./build/cjs/webpack/index.js"
},
- "./webpack5": {
- "types": "./build/types/webpack/webpack5.d.ts",
- "import": "./build/esm/webpack/webpack5.js",
- "require": "./build/cjs/webpack/webpack5.js"
- },
"./rollup": {
"types": "./build/types/rollup/index.d.ts",
"import": "./build/esm/rollup/index.js",
@@ -115,12 +110,13 @@
"dotenv": "^17.4.2",
"glob": "^13.0.6",
"magic-string": "~0.30.8",
+ "oxc-parser": "^0.143.0",
"sentry": "^0.44.0",
"supports-color": "^8.1.1"
},
"peerDependencies": {
"rollup": ">=3.2.0",
- "webpack": ">=5.0.0"
+ "webpack": ">=5.1.0"
},
"peerDependenciesMeta": {
"rollup": {
@@ -134,7 +130,6 @@
"@babel/preset-react": "^7.23.3",
"@types/babel__core": "^7.20.5",
"@types/node": "^18.6.3",
- "@types/webpack": "npm:@types/webpack@^4",
"premove": "^4.0.0",
"rolldown": "^1.0.0",
"vitest": "^3.2.7",
diff --git a/packages/bundler-plugins/rollup.npm.config.mjs b/packages/bundler-plugins/rollup.npm.config.mjs
index bd0f18d8a23d..fe1cb44cbe76 100644
--- a/packages/bundler-plugins/rollup.npm.config.mjs
+++ b/packages/bundler-plugins/rollup.npm.config.mjs
@@ -8,7 +8,6 @@ export default makeNPMConfigVariants(
'src/vite/index.ts',
'src/esbuild/index.ts',
'src/webpack/index.ts',
- 'src/webpack/webpack5.ts',
'src/webpack/component-annotation-transform.ts',
'src/babel-plugin/index.ts',
],
diff --git a/packages/bundler-plugins/src/core/component-annotation-vite-ast.ts b/packages/bundler-plugins/src/core/component-annotation-oxc-ast.ts
similarity index 100%
rename from packages/bundler-plugins/src/core/component-annotation-vite-ast.ts
rename to packages/bundler-plugins/src/core/component-annotation-oxc-ast.ts
diff --git a/packages/bundler-plugins/src/core/component-annotation-vite-fragments.ts b/packages/bundler-plugins/src/core/component-annotation-oxc-fragments.ts
similarity index 97%
rename from packages/bundler-plugins/src/core/component-annotation-vite-fragments.ts
rename to packages/bundler-plugins/src/core/component-annotation-oxc-fragments.ts
index 5ce47fd58491..8baa7d027855 100644
--- a/packages/bundler-plugins/src/core/component-annotation-vite-fragments.ts
+++ b/packages/bundler-plugins/src/core/component-annotation-oxc-fragments.ts
@@ -1,6 +1,6 @@
-import type { AstNode, FragmentContext } from './component-annotation-vite-ast';
-import { isAstNode, isObjectLike, walkAst } from './component-annotation-vite-ast';
-import { getStringName } from './component-annotation-vite-jsx';
+import type { AstNode, FragmentContext } from './component-annotation-oxc-ast';
+import { isAstNode, isObjectLike, walkAst } from './component-annotation-oxc-ast';
+import { getStringName } from './component-annotation-oxc-jsx';
export function collectFragmentContext(ast: AstNode): FragmentContext {
const context: FragmentContext = {
diff --git a/packages/bundler-plugins/src/core/component-annotation-vite-jsx.ts b/packages/bundler-plugins/src/core/component-annotation-oxc-jsx.ts
similarity index 99%
rename from packages/bundler-plugins/src/core/component-annotation-vite-jsx.ts
rename to packages/bundler-plugins/src/core/component-annotation-oxc-jsx.ts
index 7c5ecd6a2571..78fd40d99ae6 100644
--- a/packages/bundler-plugins/src/core/component-annotation-vite-jsx.ts
+++ b/packages/bundler-plugins/src/core/component-annotation-oxc-jsx.ts
@@ -12,8 +12,8 @@ import type {
JSXFragmentNode,
JSXOpeningElementNode,
JSXRootNode,
-} from './component-annotation-vite-ast';
-import { isAstNode, isObjectLike } from './component-annotation-vite-ast';
+} from './component-annotation-oxc-ast';
+import { isAstNode, isObjectLike } from './component-annotation-oxc-ast';
const UNKNOWN_ELEMENT_NAME = 'unknown';
const WEB_ATTRIBUTE_NAMES = [WEB_ELEMENT_NAME, WEB_COMPONENT_NAME, WEB_SOURCE_FILE_NAME] as const;
diff --git a/packages/bundler-plugins/src/core/component-annotation-vite-walk.ts b/packages/bundler-plugins/src/core/component-annotation-oxc-walk.ts
similarity index 94%
rename from packages/bundler-plugins/src/core/component-annotation-vite-walk.ts
rename to packages/bundler-plugins/src/core/component-annotation-oxc-walk.ts
index 420b6ac8033a..7425d145dd4e 100644
--- a/packages/bundler-plugins/src/core/component-annotation-vite-walk.ts
+++ b/packages/bundler-plugins/src/core/component-annotation-oxc-walk.ts
@@ -1,17 +1,17 @@
-import type { AstNode, AttributeInsertion, FragmentContext, JSXRootNode } from './component-annotation-vite-ast';
+import type { AstNode, AttributeInsertion, FragmentContext, JSXRootNode } from './component-annotation-oxc-ast';
import {
addPendingAttributes,
getStringName,
isJSXElement,
isJSXRoot,
toAttributeInsertions,
-} from './component-annotation-vite-jsx';
-import { isAstNode, isObjectLike, walkAst } from './component-annotation-vite-ast';
-import { collectFragmentContext } from './component-annotation-vite-fragments';
+} from './component-annotation-oxc-jsx';
+import { isAstNode, isObjectLike, walkAst } from './component-annotation-oxc-ast';
+import { collectFragmentContext } from './component-annotation-oxc-fragments';
type ComponentJSXRoots = { name: string; roots: JSXRootNode[] };
-export function collectViteComponentAnnotationInsertions(
+export function collectOxcComponentAnnotationInsertions(
code: string,
ast: AstNode,
ignoredComponents: string[],
diff --git a/packages/bundler-plugins/src/core/component-annotation-vite.ts b/packages/bundler-plugins/src/core/component-annotation-oxc.ts
similarity index 74%
rename from packages/bundler-plugins/src/core/component-annotation-vite.ts
rename to packages/bundler-plugins/src/core/component-annotation-oxc.ts
index a0ce0907268e..8bb27f8a7963 100644
--- a/packages/bundler-plugins/src/core/component-annotation-vite.ts
+++ b/packages/bundler-plugins/src/core/component-annotation-oxc.ts
@@ -4,26 +4,50 @@ import MagicString from 'magic-string';
import { KNOWN_INCOMPATIBLE_PLUGINS } from '../babel-plugin/constants';
import { stripQueryAndHashFromPath } from './utils';
-import { isAstNode } from './component-annotation-vite-ast';
-import { collectViteComponentAnnotationInsertions } from './component-annotation-vite-walk';
+import { isAstNode } from './component-annotation-oxc-ast';
+import { collectOxcComponentAnnotationInsertions } from './component-annotation-oxc-walk';
import type {
AttributeInsertion,
ComponentAnnotationTransformMeta,
ComponentAnnotationTransformResult,
MagicStringLike,
ParseAstAsync,
-} from './component-annotation-vite-ast';
+} from './component-annotation-oxc-ast';
export type {
ComponentAnnotationTransformMeta,
ComponentAnnotationTransformResult,
-} from './component-annotation-vite-ast';
+} from './component-annotation-oxc-ast';
+
+let oxcParseAstAsyncPromise: Promise | undefined;
+
+export function getOxcParseAstAsync(): Promise {
+ if (!oxcParseAstAsyncPromise) {
+ oxcParseAstAsyncPromise = import('oxc-parser')
+ .then(({ parse }): ParseAstAsync => {
+ return async (code, { lang }) => {
+ // preserveParens: false matches the AST Vite 8 produces. The walker
+ // does not look through ParenthesizedExpression nodes.
+ const { program, errors } = await parse(`component.${lang}`, code, { lang, preserveParens: false });
+
+ if (errors.length > 0) {
+ throw new Error(errors[0]?.message);
+ }
+
+ return program;
+ };
+ })
+ .catch(() => null);
+ }
+
+ return oxcParseAstAsyncPromise;
+}
// Keep this as a superset of JSX tag starts Babel can annotate, because a miss suppresses Babel fallback.
const JSX_TAG_START_REGEXP = /<[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}.:-]*|<>/u;
const JSX_FILE_REGEXP = /\.[jt]sx$/;
-function isViteAnnotationFile(idWithoutQueryAndHash: string): boolean {
+function isAnnotationFile(idWithoutQueryAndHash: string): boolean {
if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) {
return false;
}
@@ -72,7 +96,7 @@ function getMagicString(
return { magicString: new MagicString(code), isNative: false };
}
-async function annotateWithViteParser(
+async function annotateWithOxcParser(
code: string,
id: string,
ignoredComponents: string[],
@@ -83,7 +107,7 @@ async function annotateWithViteParser(
if (
!idWithoutQueryAndHash ||
- !isViteAnnotationFile(idWithoutQueryAndHash) ||
+ !isAnnotationFile(idWithoutQueryAndHash) ||
!shouldTryParse(code) ||
shouldSkipIncompatibleFile(idWithoutQueryAndHash)
) {
@@ -100,7 +124,7 @@ async function annotateWithViteParser(
return undefined;
}
- const insertions = collectViteComponentAnnotationInsertions(
+ const insertions = collectOxcComponentAnnotationInsertions(
code,
ast,
ignoredComponents,
@@ -132,7 +156,7 @@ async function annotateWithViteParser(
};
}
-export function createViteComponentNameAnnotateHooks(
+export function createOxcComponentNameAnnotateHooks(
ignoredComponents: string[],
getParseAstAsync: () => Promise,
): {
@@ -151,7 +175,7 @@ export function createViteComponentNameAnnotateHooks(
return undefined;
}
- return await annotateWithViteParser(code, id, ignoredComponents, parseAstAsync, meta);
+ return await annotateWithOxcParser(code, id, ignoredComponents, parseAstAsync, meta);
} catch {
return undefined;
}
diff --git a/packages/bundler-plugins/src/core/sentry/telemetry.ts b/packages/bundler-plugins/src/core/sentry/telemetry.ts
index a68f106e07b9..72ea587bfef8 100644
--- a/packages/bundler-plugins/src/core/sentry/telemetry.ts
+++ b/packages/bundler-plugins/src/core/sentry/telemetry.ts
@@ -27,7 +27,7 @@ export function createSentryInstance(
dsn: 'https://4c2bae7d9fbc413e8f7385f55c515d51@o1.ingest.sentry.io/6690737',
- tracesSampleRate: 1,
+ tracesSampleRate: 0.3,
traceLifecycle: 'static',
sampleRate: 1,
@@ -64,6 +64,11 @@ export function createSentryInstance(
const scope = new Scope();
scope.setClient(client);
+ // Integration tests snapshot the emitted transaction, so the sampling decision must not depend on chance.
+ if (process.env['SENTRY_TEST_OUT_DIR']) {
+ scope.setPropagationContext({ ...scope.getPropagationContext(), sampleRand: 0 });
+ }
+
setTelemetryDataOnScope(options, scope, buildTool, buildToolMajorVersion);
return { sentryScope: scope, sentryClient: client };
diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts
index 22fe5a295046..1bb1def0b80d 100644
--- a/packages/bundler-plugins/src/rollup/index.ts
+++ b/packages/bundler-plugins/src/rollup/index.ts
@@ -18,7 +18,7 @@ import {
import type {
ComponentAnnotationTransformMeta,
ComponentAnnotationTransformResult,
-} from '../core/component-annotation-vite';
+} from '../core/component-annotation-oxc';
import type { SourceMap } from 'magic-string';
import MagicString from 'magic-string';
import * as path from 'node:path';
@@ -41,7 +41,7 @@ type ViteModule = {
};
type ViteParseAstAsync = NonNullable;
-type ViteAnnotationHooks = {
+type FastAnnotationHooks = {
transform(
code: string,
id: string,
@@ -168,27 +168,27 @@ export function _rollupPluginInternal(
!!options.reactComponentAnnotation?._experimentalInjectIntoHtml,
)
: undefined;
- const transformViteAnnotations =
- options.reactComponentAnnotation?.enabled &&
- buildTool === 'vite' &&
- buildToolMajorVersion === '8' &&
- !options.reactComponentAnnotation?._experimentalInjectIntoHtml
+ const transformFastAnnotations =
+ options.reactComponentAnnotation?.enabled && !options.reactComponentAnnotation?._experimentalInjectIntoHtml
? (() => {
- let viteAnnotationHooksPromise: Promise | undefined;
+ let fastAnnotationHooksPromise: Promise | undefined;
return {
transform(code: string, id: string, meta?: ComponentAnnotationTransformMeta) {
- if (!viteAnnotationHooksPromise) {
- viteAnnotationHooksPromise = import('../core/component-annotation-vite').then(
- ({ createViteComponentNameAnnotateHooks }) =>
- createViteComponentNameAnnotateHooks(
+ if (!fastAnnotationHooksPromise) {
+ fastAnnotationHooksPromise = import('../core/component-annotation-oxc').then(
+ ({ createOxcComponentNameAnnotateHooks, getOxcParseAstAsync }) =>
+ createOxcComponentNameAnnotateHooks(
options.reactComponentAnnotation?.ignoredComponents || [],
- getViteParseAstAsync,
+ // Vite 8 already loads an oxc-based parser, so reuse it.
+ buildTool === 'vite' && buildToolMajorVersion === '8'
+ ? getViteParseAstAsync
+ : getOxcParseAstAsync,
),
);
}
- return viteAnnotationHooksPromise.then(hooks => hooks.transform(code, id, meta));
+ return fastAnnotationHooksPromise.then(hooks => hooks.transform(code, id, meta));
},
};
})()
@@ -212,8 +212,8 @@ export function _rollupPluginInternal(
// only in Sentry code. If we successfully add annotations, we can return early.
let shouldRunBabelAnnotations = true;
- if (transformViteAnnotations?.transform) {
- const result = await transformViteAnnotations.transform(code, id, meta);
+ if (transformFastAnnotations?.transform) {
+ const result = await transformFastAnnotations.transform(code, id, meta);
if (result) {
return result;
}
diff --git a/packages/bundler-plugins/src/webpack/index.ts b/packages/bundler-plugins/src/webpack/index.ts
index 634f2c1e958f..0597b5794c08 100644
--- a/packages/bundler-plugins/src/webpack/index.ts
+++ b/packages/bundler-plugins/src/webpack/index.ts
@@ -1,37 +1,353 @@
-import type { SentryWebpackPluginOptions } from './webpack4and5';
-import { sentryWebpackPluginFactory } from './webpack4and5';
+import type { Options } from '../core/index';
+import {
+ createSentryBuildPluginManager,
+ generateReleaseInjectorCode,
+ generateModuleMetadataInjectorCode,
+ stringToUUID,
+ createComponentNameAnnotateHooks,
+ CodeInjection,
+ getDebugIdSnippet,
+ createDebugIdUploadFunction,
+ isJsFile,
+ stampDebugId,
+} from '../core/index';
+import * as path from 'node:path';
+import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
+import { randomUUID } from 'node:crypto';
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type PluginClass = new (options: any) => unknown;
+const _req = createRequire(import.meta.url);
+
+// Resolve the loader path via the package's own exports.
+// This module may end up in a shared chunk (_chunks/) whose import.meta.url
+// does not point to the webpack/ directory where the transform file lives, so
+// a path-relative lookup would fail. Using require.resolve on the package export
+// always finds the correct installed file regardless of chunk placement.
+let COMPONENT_ANNOTATION_LOADER: string;
+try {
+ COMPONENT_ANNOTATION_LOADER = _req.resolve('@sentry/bundler-plugins/webpack-loader');
+} catch {
+ // Fallback for non-packaged environments (e.g., monorepo source runs without dist)
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore Rollup transpiles import.meta for us for CJS
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
+ // The Rollup build emits `.js` for both CJS and ESM, so the extension is the same in both.
+ COMPONENT_ANNOTATION_LOADER = path.resolve(dirname, 'component-annotation-transform.js');
+}
+
+interface BannerPluginCallbackArg {
+ chunk?: {
+ hash?: string;
+ contentHash?: {
+ javascript?: string;
+ };
+ };
+}
+
+type PluginClass = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ new (options: any): unknown;
+};
+
+type WebpackSource = {
+ source: () => string | Buffer;
+};
+
+type WebpackRawSource = {
+ new (source: string): WebpackSource;
+};
+
+type WebpackAsset = {
+ name: string;
+ source: WebpackSource;
+ info: {
+ related?: {
+ sourceMap?: string | string[];
+ };
+ };
+};
+
+type WebpackCompiler = {
+ options: {
+ plugins?: unknown[];
+ mode?: string;
+ module?: {
+ rules?: unknown[];
+ };
+ };
+ hooks: {
+ thisCompilation: {
+ tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void;
+ };
+ afterEmit: {
+ tapAsync: (name: string, callback: (compilation: WebpackCompilation, cb: () => void) => void) => void;
+ };
+ done: {
+ tap: (name: string, callback: () => void) => void;
+ };
+ };
+ webpack?: {
+ BannerPlugin?: PluginClass;
+ DefinePlugin?: PluginClass;
+ Compilation?: {
+ PROCESS_ASSETS_STAGE_DEV_TOOLING?: number;
+ };
+ sources?: {
+ RawSource?: WebpackRawSource;
+ };
+ };
+};
+
+type WebpackCompilation = {
+ outputOptions: {
+ path?: string;
+ };
+ assets: Record;
+ getAssets: () => WebpackAsset[];
+ getAsset: (name: string) => WebpackAsset | undefined;
+ updateAsset: (name: string, source: WebpackSource) => void;
+ hooks: {
+ processAssets: {
+ tap: (options: { name: string; stage: number }, callback: () => void) => void;
+ };
+ };
+};
type WebpackModule = {
- BannerPlugin?: PluginClass;
- DefinePlugin?: PluginClass;
- default?: WebpackModule;
+ version?: string;
+ default?: { version?: string };
};
-// `webpack` is an optional peer dependency. We require it lazily so the plugin doesn't
-// crash on load in bundlers that don't ship `webpack` (e.g. rspack) — those provide
-// the plugin classes via `compiler.webpack` at runtime instead.
-function loadWebpack(): WebpackModule {
+// Only used for telemetry; `webpack` is an optional peer dependency and may be absent (e.g. rspack).
+function getWebpackMajorVersion(): string | undefined {
try {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore Rollup transpiles import.meta for CJS
- return createRequire(import.meta.url)('webpack') as WebpackModule;
+ const webpack = _req('webpack') as WebpackModule;
+ const version = webpack.version ?? webpack.default?.version;
+ return version?.split('.')[0];
} catch {
- return {};
+ return undefined;
+ }
+}
+
+/**
+ * Stamps each JS asset's debug ID into the asset itself and its source map asset.
+ *
+ * Runs after source maps have been generated, so the JS asset no longer needs to carry
+ * source map information and can be replaced with a plain `RawSource`.
+ */
+function addDebugIdsToAssets(compilation: WebpackCompilation, RawSource: WebpackRawSource): void {
+ for (const asset of compilation.getAssets()) {
+ if (!isJsFile(asset.name)) {
+ continue;
+ }
+
+ const bundleSource = asset.source.source().toString();
+ const relatedSourceMap = asset.info.related?.sourceMap;
+ const sourceMapName = typeof relatedSourceMap === 'string' ? relatedSourceMap : `${asset.name}.map`;
+ const sourceMapAsset = compilation.getAsset(sourceMapName);
+
+ const stamped = stampDebugId(bundleSource, sourceMapAsset?.source.source().toString());
+ if (!stamped) {
+ continue;
+ }
+
+ compilation.updateAsset(asset.name, new RawSource(stamped.bundleSource));
+ if (stamped.sourceMapSource !== undefined) {
+ compilation.updateAsset(sourceMapName, new RawSource(stamped.sourceMapSource));
+ }
}
}
-const webpack = loadWebpack();
-const BannerPlugin = webpack.BannerPlugin ?? webpack.default?.BannerPlugin;
-const DefinePlugin = webpack.DefinePlugin ?? webpack.default?.DefinePlugin;
+function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) {
+ const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, {
+ loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? '[sentry-webpack-plugin]',
+ buildTool: 'webpack',
+ buildToolMajorVersion: getWebpackMajorVersion(),
+ });
+
+ const {
+ logger,
+ normalizedOptions: options,
+ bundleSizeOptimizationReplacementValues: replacementValues,
+ bundleMetadata,
+ createDependencyOnBuildArtifacts,
+ } = sentryBuildPluginManager;
+
+ if (options.disable) {
+ return {
+ apply() {
+ // noop plugin
+ },
+ };
+ }
+
+ if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) {
+ logger.warn('Running Sentry plugin from within a `node_modules` folder. Some features may not work.');
+ }
+
+ const sourcemapsEnabled = options.sourcemaps?.disable !== true;
+ const staticInjectionCode = new CodeInjection();
+
+ if (!options.release.inject) {
+ logger.debug('Release injection disabled via `release.inject` option. Will not inject release.');
+ } else if (!options.release.name) {
+ logger.debug(
+ 'No release name provided. Will not inject release. Please set the `release.name` option to identify your release.',
+ );
+ } else {
+ staticInjectionCode.append(
+ generateReleaseInjectorCode({
+ release: options.release.name,
+ injectBuildInformation: options._experiments.injectBuildInformation || false,
+ }),
+ );
+ }
+
+ if (Object.keys(bundleMetadata).length > 0) {
+ staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata));
+ }
+
+ const transformAnnotations = options.reactComponentAnnotation?.enabled
+ ? createComponentNameAnnotateHooks(
+ options.reactComponentAnnotation?.ignoredComponents || [],
+ !!options.reactComponentAnnotation?._experimentalInjectIntoHtml,
+ )
+ : undefined;
+
+ const transformReplace = Object.keys(replacementValues).length > 0;
+
+ return {
+ apply(compiler: WebpackCompiler) {
+ void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => {
+ // Telemetry failures are acceptable
+ });
+
+ const { BannerPlugin, DefinePlugin } = compiler.webpack ?? {};
+
+ // Add BannerPlugin for code injection (release, metadata, debug IDs)
+ if (!staticInjectionCode.isEmpty() || sourcemapsEnabled) {
+ if (!BannerPlugin) {
+ logger.warn(
+ 'BannerPlugin is not available. Skipping code injection. This usually means webpack is not properly configured.',
+ );
+ } else {
+ compiler.options.plugins = compiler.options.plugins || [];
+ compiler.options.plugins.push(
+ new BannerPlugin({
+ raw: true,
+ include: /\.(js|ts|jsx|tsx|mjs|cjs)(\?[^?]*)?(#[^#]*)?$/,
+ banner: (arg?: BannerPluginCallbackArg) => {
+ const codeToInject = staticInjectionCode.clone();
+ if (sourcemapsEnabled) {
+ const hash = arg?.chunk?.contentHash?.javascript ?? arg?.chunk?.hash;
+ const debugId = hash ? stringToUUID(hash) : randomUUID();
+ codeToInject.append(getDebugIdSnippet(debugId));
+ }
+ return codeToInject.code();
+ },
+ }),
+ );
+ }
+ }
+
+ // The upload routine (which stamps debug IDs into temp copies of the artifacts) is skipped
+ // with `disable-upload`, so the emitted artifacts get stamped in the asset pipeline instead.
+ if (sourcemapsEnabled && options.sourcemaps?.disable === 'disable-upload') {
+ const RawSource = compiler.webpack?.sources?.RawSource;
+ // Right after source map generation (and thus after minification, which would strip the comment),
+ // so later stages (real content hashing, subresource integrity) see the final assets.
+ const stage = (compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_DEV_TOOLING ?? 500) + 1;
+
+ if (!RawSource) {
+ logger.warn(
+ 'Webpack sources are not available. Skipping debug ID injection into emitted source maps. This usually means webpack is not properly configured.',
+ );
+ } else {
+ compiler.hooks.thisCompilation.tap('sentry-webpack-plugin', compilation => {
+ compilation.hooks.processAssets.tap({ name: 'sentry-webpack-plugin', stage }, () => {
+ addDebugIdsToAssets(compilation, RawSource);
+ });
+ });
+ }
+ }
+
+ // Add DefinePlugin for bundle size optimizations
+ if (transformReplace && DefinePlugin) {
+ compiler.options.plugins = compiler.options.plugins || [];
+ compiler.options.plugins.push(new DefinePlugin(replacementValues));
+ }
+
+ // Add component name annotation transform
+ if (transformAnnotations?.transform) {
+ compiler.options.module = compiler.options.module || {};
+ compiler.options.module.rules = compiler.options.module.rules || [];
+ compiler.options.module.rules.unshift({
+ test: /\.[jt]sx$/,
+ exclude: /node_modules/,
+ enforce: 'pre',
+ use: [
+ {
+ loader: COMPONENT_ANNOTATION_LOADER,
+ options: {
+ transform: transformAnnotations.transform,
+ },
+ },
+ ],
+ });
+ }
+
+ compiler.hooks.afterEmit.tapAsync(
+ 'sentry-webpack-plugin',
+ (compilation: WebpackCompilation, callback: (err?: Error) => void) => {
+ const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();
+ const upload = createDebugIdUploadFunction({ sentryBuildPluginManager });
+
+ const run = async (): Promise => {
+ try {
+ await sentryBuildPluginManager.createRelease();
+ if (sourcemapsEnabled && options.sourcemaps?.disable !== 'disable-upload') {
+ const outputPath = compilation.outputOptions.path ?? path.resolve();
+ const buildArtifacts = Object.keys(compilation.assets).map(asset => path.join(outputPath, asset));
+ await upload(buildArtifacts);
+ }
+ } finally {
+ freeGlobalDependencyOnBuildArtifacts();
+ await sentryBuildPluginManager.deleteArtifacts();
+ }
+ };
+
+ run().then(
+ () => callback(),
+ (err: Error) => callback(err),
+ );
+ },
+ );
+
+ if (userOptions._experiments?.forceExitOnBuildCompletion && compiler.options.mode === 'production') {
+ compiler.hooks.done.tap('sentry-webpack-plugin', () => {
+ setTimeout(() => {
+ logger.debug('Exiting process after debug file upload');
+ process.exit(0);
+ });
+ });
+ }
+ },
+ };
+}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = sentryWebpackPluginFactory({
- BannerPlugin,
- DefinePlugin,
-});
+export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = createSentryWebpackPlugin;
-export type { SentryWebpackPluginOptions };
+export type SentryWebpackPluginOptions = Options & {
+ _experiments?: Options['_experiments'] & {
+ /**
+ * If enabled, the webpack plugin will exit the build process after the build completes.
+ * Use this with caution, as it will terminate the process.
+ *
+ * More information: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/345
+ *
+ * @default false
+ */
+ forceExitOnBuildCompletion?: boolean;
+ };
+};
diff --git a/packages/bundler-plugins/src/webpack/webpack4and5.ts b/packages/bundler-plugins/src/webpack/webpack4and5.ts
deleted file mode 100644
index 42c635e37bb2..000000000000
--- a/packages/bundler-plugins/src/webpack/webpack4and5.ts
+++ /dev/null
@@ -1,375 +0,0 @@
-import type { Options } from '../core/index';
-import {
- createSentryBuildPluginManager,
- generateReleaseInjectorCode,
- generateModuleMetadataInjectorCode,
- stringToUUID,
- createComponentNameAnnotateHooks,
- CodeInjection,
- getDebugIdSnippet,
- createDebugIdUploadFunction,
- isJsFile,
- stampDebugId,
-} from '../core/index';
-import * as path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import { createRequire } from 'node:module';
-import { randomUUID } from 'node:crypto';
-
-const _req = createRequire(import.meta.url);
-
-// Resolve the loader path via the package's own exports.
-// webpack4and5.ts may end up in a shared chunk (_chunks/) whose import.meta.url
-// does not point to the webpack/ directory where the transform file lives, so
-// a path-relative lookup would fail. Using require.resolve on the package export
-// always finds the correct installed file regardless of chunk placement.
-let COMPONENT_ANNOTATION_LOADER: string;
-try {
- COMPONENT_ANNOTATION_LOADER = _req.resolve('@sentry/bundler-plugins/webpack-loader');
-} catch {
- // Fallback for non-packaged environments (e.g., monorepo source runs without dist)
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore Rollup transpiles import.meta for us for CJS
- const dirname = path.dirname(fileURLToPath(import.meta.url));
- // The Rollup build emits `.js` for both CJS and ESM, so the extension is the same in both.
- COMPONENT_ANNOTATION_LOADER = path.resolve(dirname, 'component-annotation-transform.js');
-}
-
-// since webpack 5.1 compiler contains webpack module so plugins always use correct webpack version
-// https://github.com/webpack/webpack/commit/65eca2e529ce1d79b79200d4bdb1ce1b81141459
-
-interface BannerPluginCallbackArg {
- chunk?: {
- hash?: string;
- contentHash?: {
- javascript?: string;
- };
- };
-}
-
-type UnsafeBannerPlugin = {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- new (options: any): unknown;
-};
-
-type UnsafeDefinePlugin = {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- new (options: any): unknown;
-};
-
-type WebpackSource = {
- source: () => string | Buffer;
-};
-
-type WebpackRawSource = {
- new (source: string): WebpackSource;
-};
-
-type WebpackAsset = {
- name: string;
- source: WebpackSource;
- info: {
- related?: {
- sourceMap?: string | string[];
- };
- };
-};
-
-type WebpackCompiler = {
- options: {
- plugins?: unknown[];
- mode?: string;
- module?: {
- rules?: unknown[];
- };
- };
- hooks: {
- thisCompilation: {
- tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void;
- };
- afterEmit: {
- tapAsync: (name: string, callback: (compilation: WebpackCompilation, cb: () => void) => void) => void;
- };
- done: {
- tap: (name: string, callback: () => void) => void;
- };
- };
- webpack?: {
- BannerPlugin?: UnsafeBannerPlugin;
- DefinePlugin?: UnsafeDefinePlugin;
- Compilation?: {
- PROCESS_ASSETS_STAGE_DEV_TOOLING?: number;
- };
- sources?: {
- RawSource?: WebpackRawSource;
- };
- };
-};
-
-type WebpackCompilation = {
- outputOptions: {
- path?: string;
- };
- assets: Record;
- getAssets: () => WebpackAsset[];
- getAsset: (name: string) => WebpackAsset | undefined;
- updateAsset: (name: string, source: WebpackSource) => void;
- hooks: {
- processAssets: {
- tap: (options: { name: string; stage: number }, callback: () => void) => void;
- };
- };
-};
-
-// Detect webpack major version for telemetry (helps differentiate webpack 4 vs 5 usage)
-function getWebpackMajorVersion(): string | undefined {
- try {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore - Rollup already transpiles this for us
- const req = createRequire(import.meta.url);
- const webpack = req('webpack') as { version?: string; default?: { version?: string } };
- const version = webpack?.version ?? webpack?.default?.version;
- const webpackMajorVersion = version?.split('.')[0]; // "4" or "5"
- return webpackMajorVersion;
- } catch {
- return undefined;
- }
-}
-
-/**
- * Stamps each JS asset's debug ID into the asset itself and its source map asset.
- *
- * Runs after source maps have been generated, so the JS asset no longer needs to carry
- * source map information and can be replaced with a plain `RawSource`.
- */
-function addDebugIdsToAssets(compilation: WebpackCompilation, RawSource: WebpackRawSource): void {
- for (const asset of compilation.getAssets()) {
- if (!isJsFile(asset.name)) {
- continue;
- }
-
- const bundleSource = asset.source.source().toString();
- const relatedSourceMap = asset.info.related?.sourceMap;
- const sourceMapName = typeof relatedSourceMap === 'string' ? relatedSourceMap : `${asset.name}.map`;
- const sourceMapAsset = compilation.getAsset(sourceMapName);
-
- const stamped = stampDebugId(bundleSource, sourceMapAsset?.source.source().toString());
- if (!stamped) {
- continue;
- }
-
- compilation.updateAsset(asset.name, new RawSource(stamped.bundleSource));
- if (stamped.sourceMapSource !== undefined) {
- compilation.updateAsset(sourceMapName, new RawSource(stamped.sourceMapSource));
- }
- }
-}
-
-/**
- * The factory function accepts BannerPlugin and DefinePlugin classes in
- * order to avoid direct dependencies on webpack.
- *
- * This allow us to export version of the plugin for webpack 5.1+ and compatible environments.
- *
- * Since webpack 5.1 compiler contains webpack module so plugins always use correct webpack version.
- */
-export function sentryWebpackPluginFactory({
- BannerPlugin: UnsafeBannerPlugin,
- DefinePlugin: UnsafeDefinePlugin,
-}: {
- BannerPlugin?: UnsafeBannerPlugin;
- DefinePlugin?: UnsafeDefinePlugin;
-} = {}) {
- return function sentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) {
- const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, {
- loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? '[sentry-webpack-plugin]',
- buildTool: 'webpack',
- buildToolMajorVersion: getWebpackMajorVersion(),
- });
-
- const {
- logger,
- normalizedOptions: options,
- bundleSizeOptimizationReplacementValues: replacementValues,
- bundleMetadata,
- createDependencyOnBuildArtifacts,
- } = sentryBuildPluginManager;
-
- if (options.disable) {
- return {
- apply() {
- // noop plugin
- },
- };
- }
-
- if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) {
- logger.warn('Running Sentry plugin from within a `node_modules` folder. Some features may not work.');
- }
-
- const sourcemapsEnabled = options.sourcemaps?.disable !== true;
- const staticInjectionCode = new CodeInjection();
-
- if (!options.release.inject) {
- logger.debug('Release injection disabled via `release.inject` option. Will not inject release.');
- } else if (!options.release.name) {
- logger.debug(
- 'No release name provided. Will not inject release. Please set the `release.name` option to identify your release.',
- );
- } else {
- staticInjectionCode.append(
- generateReleaseInjectorCode({
- release: options.release.name,
- injectBuildInformation: options._experiments.injectBuildInformation || false,
- }),
- );
- }
-
- if (Object.keys(bundleMetadata).length > 0) {
- staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata));
- }
-
- const transformAnnotations = options.reactComponentAnnotation?.enabled
- ? createComponentNameAnnotateHooks(
- options.reactComponentAnnotation?.ignoredComponents || [],
- !!options.reactComponentAnnotation?._experimentalInjectIntoHtml,
- )
- : undefined;
-
- const transformReplace = Object.keys(replacementValues).length > 0;
-
- return {
- apply(compiler: WebpackCompiler) {
- void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => {
- // Telemetry failures are acceptable
- });
-
- // Get the correct plugin classes (webpack 5.1+ vs older versions)
- const BannerPlugin = compiler?.webpack?.BannerPlugin || UnsafeBannerPlugin;
- const DefinePlugin = compiler?.webpack?.DefinePlugin || UnsafeDefinePlugin;
-
- // Add BannerPlugin for code injection (release, metadata, debug IDs)
- if (!staticInjectionCode.isEmpty() || sourcemapsEnabled) {
- if (!BannerPlugin) {
- logger.warn(
- 'BannerPlugin is not available. Skipping code injection. This usually means webpack is not properly configured.',
- );
- } else {
- compiler.options.plugins = compiler.options.plugins || [];
- compiler.options.plugins.push(
- new BannerPlugin({
- raw: true,
- include: /\.(js|ts|jsx|tsx|mjs|cjs)(\?[^?]*)?(#[^#]*)?$/,
- banner: (arg?: BannerPluginCallbackArg) => {
- const codeToInject = staticInjectionCode.clone();
- if (sourcemapsEnabled) {
- const hash = arg?.chunk?.contentHash?.javascript ?? arg?.chunk?.hash;
- const debugId = hash ? stringToUUID(hash) : randomUUID();
- codeToInject.append(getDebugIdSnippet(debugId));
- }
- return codeToInject.code();
- },
- }),
- );
- }
- }
-
- // The upload routine (which stamps debug IDs into temp copies of the artifacts) is skipped
- // with `disable-upload`, so the emitted artifacts get stamped in the asset pipeline instead.
- if (sourcemapsEnabled && options.sourcemaps?.disable === 'disable-upload') {
- const RawSource = compiler.webpack?.sources?.RawSource;
- // Right after source map generation (and thus after minification, which would strip the comment),
- // so later stages (real content hashing, subresource integrity) see the final assets.
- const stage = (compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_DEV_TOOLING ?? 500) + 1;
-
- if (!RawSource) {
- logger.warn(
- 'Webpack sources are not available. Skipping debug ID injection into emitted source maps. This usually means webpack is not properly configured.',
- );
- } else {
- compiler.hooks.thisCompilation.tap('sentry-webpack-plugin', compilation => {
- compilation.hooks.processAssets.tap({ name: 'sentry-webpack-plugin', stage }, () => {
- addDebugIdsToAssets(compilation, RawSource);
- });
- });
- }
- }
-
- // Add DefinePlugin for bundle size optimizations
- if (transformReplace && DefinePlugin) {
- compiler.options.plugins = compiler.options.plugins || [];
- compiler.options.plugins.push(new DefinePlugin(replacementValues));
- }
-
- // Add component name annotation transform
- if (transformAnnotations?.transform) {
- compiler.options.module = compiler.options.module || {};
- compiler.options.module.rules = compiler.options.module.rules || [];
- compiler.options.module.rules.unshift({
- test: /\.[jt]sx$/,
- exclude: /node_modules/,
- enforce: 'pre',
- use: [
- {
- loader: COMPONENT_ANNOTATION_LOADER,
- options: {
- transform: transformAnnotations.transform,
- },
- },
- ],
- });
- }
-
- compiler.hooks.afterEmit.tapAsync(
- 'sentry-webpack-plugin',
- (compilation: WebpackCompilation, callback: (err?: Error) => void) => {
- const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts();
- const upload = createDebugIdUploadFunction({ sentryBuildPluginManager });
-
- const run = async (): Promise => {
- try {
- await sentryBuildPluginManager.createRelease();
- if (sourcemapsEnabled && options.sourcemaps?.disable !== 'disable-upload') {
- const outputPath = compilation.outputOptions.path ?? path.resolve();
- const buildArtifacts = Object.keys(compilation.assets).map(asset => path.join(outputPath, asset));
- await upload(buildArtifacts);
- }
- } finally {
- freeGlobalDependencyOnBuildArtifacts();
- await sentryBuildPluginManager.deleteArtifacts();
- }
- };
-
- run().then(
- () => callback(),
- (err: Error) => callback(err),
- );
- },
- );
-
- if (userOptions._experiments?.forceExitOnBuildCompletion && compiler.options.mode === 'production') {
- compiler.hooks.done.tap('sentry-webpack-plugin', () => {
- setTimeout(() => {
- logger.debug('Exiting process after debug file upload');
- process.exit(0);
- });
- });
- }
- },
- };
- };
-}
-
-export type SentryWebpackPluginOptions = Options & {
- _experiments?: Options['_experiments'] & {
- /**
- * If enabled, the webpack plugin will exit the build process after the build completes.
- * Use this with caution, as it will terminate the process.
- *
- * More information: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/345
- *
- * @default false
- */
- forceExitOnBuildCompletion?: boolean;
- };
-};
diff --git a/packages/bundler-plugins/src/webpack/webpack5.ts b/packages/bundler-plugins/src/webpack/webpack5.ts
deleted file mode 100644
index 063aee71da02..000000000000
--- a/packages/bundler-plugins/src/webpack/webpack5.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { SentryWebpackPluginOptions } from './webpack4and5';
-import { sentryWebpackPluginFactory } from './webpack4and5';
-
-const createSentryWebpackPlugin = sentryWebpackPluginFactory();
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = createSentryWebpackPlugin;
-
-export type { SentryWebpackPluginOptions };
diff --git a/packages/bundler-plugins/test/core/component-annotation-vite.test.ts b/packages/bundler-plugins/test/core/component-annotation-oxc.test.ts
similarity index 78%
rename from packages/bundler-plugins/test/core/component-annotation-vite.test.ts
rename to packages/bundler-plugins/test/core/component-annotation-oxc.test.ts
index 789437dd7903..2529460dfa3f 100644
--- a/packages/bundler-plugins/test/core/component-annotation-vite.test.ts
+++ b/packages/bundler-plugins/test/core/component-annotation-oxc.test.ts
@@ -5,9 +5,11 @@ import { describe, expect, it, vi } from 'vitest';
import componentNameAnnotatePlugin from '../../src/babel-plugin';
import {
- createViteComponentNameAnnotateHooks,
+ createOxcComponentNameAnnotateHooks,
+ getOxcParseAstAsync,
type ComponentAnnotationTransformResult,
-} from '../../src/core/component-annotation-vite';
+} from '../../src/core/component-annotation-oxc';
+import type { ParseAstAsync } from '../../src/core/component-annotation-oxc-ast';
type Annotation = {
elementName: string;
@@ -80,17 +82,21 @@ async function annotateWithBabel(code: string, id: string, ignoredComponents: st
return collectAnnotations(result?.code ?? '', id);
}
-async function annotateWithVite(
+async function annotateWithOxc(
code: string,
id: string,
ignoredComponents: string[] = [],
+ getParseAstAsync: () => Promise = async () => parseAstAsync,
): Promise {
- const hooks = createViteComponentNameAnnotateHooks(ignoredComponents, async () => parseAstAsync);
+ const hooks = createOxcComponentNameAnnotateHooks(ignoredComponents, getParseAstAsync);
return hooks.transform(code, id);
}
-describe('createViteComponentNameAnnotateHooks', () => {
+describe.each<[string, () => Promise]>([
+ ['@babel/parser', async () => parseAstAsync],
+ ['oxc-parser', getOxcParseAstAsync],
+])('createOxcComponentNameAnnotateHooks with %s', (_parserName, getParseAstAsync) => {
it.each([
[
'function declarations and nested children',
@@ -254,11 +260,28 @@ export function TypedComponent(props: Props) {
}`,
[],
],
+ [
+ 'tsx files with parenthesized returns and TypeScript expressions',
+ '/src/typed-parenthesized.tsx',
+ `import React from "react";
+
+type Props = { items?: T[] };
+
+export const List = (props: Props) => {
+ const items = props.items!;
+ return (
+
+ );
+};`,
+ [],
+ ],
])('matches Babel annotations for %s', async (_name, id, code, ignoredComponents) => {
- const viteResult = await annotateWithVite(code, id, ignoredComponents);
+ const oxcResult = await annotateWithOxc(code, id, ignoredComponents, getParseAstAsync);
- expect(viteResult).toBeTruthy();
- expect(collectAnnotations(viteResult?.code.toString() ?? '', id)).toEqual(
+ expect(oxcResult).toBeTruthy();
+ expect(collectAnnotations(oxcResult?.code.toString() ?? '', id)).toEqual(
await annotateWithBabel(code, id, ignoredComponents),
);
});
@@ -267,10 +290,10 @@ export function TypedComponent(props: Props) {
const code = `export const App = () => <${elementName} />;`;
const id = '/src/app.jsx';
- const viteResult = await annotateWithVite(code, id);
+ const oxcResult = await annotateWithOxc(code, id, [], getParseAstAsync);
- expect(viteResult).toBeTruthy();
- expect(collectAnnotations(viteResult?.code.toString() ?? '', id)).toEqual([
+ expect(oxcResult).toBeTruthy();
+ expect(collectAnnotations(oxcResult?.code.toString() ?? '', id)).toEqual([
{
elementName,
attributes: {
@@ -281,14 +304,16 @@ export function TypedComponent(props: Props) {
},
]);
});
+});
+describe('createOxcComponentNameAnnotateHooks', () => {
it('uses the native magicString object from transform metadata when it is available', async () => {
const code = `export function App() {
return ;
}`;
const id = '/src/app.jsx';
const magicString = new MagicString(code);
- const hooks = createViteComponentNameAnnotateHooks([], async () => parseAstAsync);
+ const hooks = createOxcComponentNameAnnotateHooks([], async () => parseAstAsync);
const result = await hooks.transform(code, id, { magicString });
@@ -296,19 +321,25 @@ export function TypedComponent(props: Props) {
expect(result?.code.toString()).toContain(`data-sentry-component="App"`);
});
- it('returns null without parsing when the file cannot contain public Vite annotations', async () => {
+ it('returns null without parsing when the file cannot contain annotations', async () => {
const parse = vi.fn(parseAstAsync);
- const hooks = createViteComponentNameAnnotateHooks([], async () => parse);
+ const hooks = createOxcComponentNameAnnotateHooks([], async () => parse);
await expect(hooks.transform('const value = 1;', '/src/app.js')).resolves.toBeNull();
expect(parse).not.toHaveBeenCalled();
});
it('returns undefined when parsing fails so callers can fall back to Babel', async () => {
- const hooks = createViteComponentNameAnnotateHooks([], async () => {
+ const hooks = createOxcComponentNameAnnotateHooks([], async () => {
throw new Error('parser unavailable');
});
await expect(hooks.transform('export const App = () => ;', '/src/app.jsx')).resolves.toBeUndefined();
});
+
+ it('returns undefined when oxc-parser reports a syntax error so callers can fall back to Babel', async () => {
+ const hooks = createOxcComponentNameAnnotateHooks([], getOxcParseAstAsync);
+
+ await expect(hooks.transform('export const App = () => ;', '/src/app.tsx')).resolves.toBeUndefined();
+ });
});
diff --git a/packages/bundler-plugins/test/rollup/public-api.test.ts b/packages/bundler-plugins/test/rollup/public-api.test.ts
index b54077fce2bc..06b8220dee2c 100644
--- a/packages/bundler-plugins/test/rollup/public-api.test.ts
+++ b/packages/bundler-plugins/test/rollup/public-api.test.ts
@@ -3,15 +3,25 @@ import { createComponentNameAnnotateHooks } from '../../src/core';
import type { Plugin, SourceMap } from 'rollup';
import { describe, it, expect, test, beforeEach, vi } from 'vitest';
-const { babelCoreImportMock, transformAsyncMock, viteAnnotationModuleImportMock, viteAnnotationTransformMock } =
- vi.hoisted(() => {
- return {
- babelCoreImportMock: vi.fn(),
- transformAsyncMock: vi.fn(async (code: string) => ({ code, map: null })),
- viteAnnotationModuleImportMock: vi.fn(),
- viteAnnotationTransformMock: vi.fn(async () => ({ code: 'fast-path', map: null })),
- };
- });
+const {
+ babelCoreImportMock,
+ transformAsyncMock,
+ annotationTransformMock,
+ createOxcComponentNameAnnotateHooksMock,
+ getOxcParseAstAsyncMock,
+} = vi.hoisted(() => {
+ const annotationTransformMock = vi.fn(async () => ({ code: 'fast-path', map: null }));
+
+ return {
+ babelCoreImportMock: vi.fn(),
+ transformAsyncMock: vi.fn(async (code: string) => ({ code, map: null })),
+ annotationTransformMock,
+ createOxcComponentNameAnnotateHooksMock: vi.fn(() => ({
+ transform: annotationTransformMock,
+ })),
+ getOxcParseAstAsyncMock: vi.fn(),
+ };
+});
vi.mock('@babel/core', () => {
babelCoreImportMock();
@@ -20,12 +30,10 @@ vi.mock('@babel/core', () => {
};
});
-vi.mock('../../src/core/component-annotation-vite', () => {
- viteAnnotationModuleImportMock();
+vi.mock('../../src/core/component-annotation-oxc', () => {
return {
- createViteComponentNameAnnotateHooks: vi.fn(() => ({
- transform: viteAnnotationTransformMock,
- })),
+ createOxcComponentNameAnnotateHooks: createOxcComponentNameAnnotateHooksMock,
+ getOxcParseAstAsync: getOxcParseAstAsyncMock,
};
});
@@ -59,29 +67,57 @@ test('component annotations only load Babel when the Babel transform runs', asyn
expect(transformAsyncMock).toHaveBeenCalledTimes(1);
});
-test('Vite annotation fast path only loads for Vite 8 annotation transforms', async () => {
- expect(viteAnnotationModuleImportMock).not.toHaveBeenCalled();
+describe('annotation fast path', () => {
+ const code = 'export function App() { return ; }';
- const vite7Plugin = _rollupPluginInternal(
- { release: { inject: false }, reactComponentAnnotation: { enabled: true } },
- 'vite',
- '7',
- ) as Plugin;
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
- await runTransform(vite7Plugin, 'export function App() { return ; }', '/src/app.jsx');
+ it.each<[string, 'rollup' | 'vite', string | undefined]>([
+ ['Rollup', 'rollup', undefined],
+ ['Vite 7', 'vite', '7'],
+ ])('uses the fast path with oxc-parser for %s', async (_name, buildTool, majorVersion) => {
+ const plugin = _rollupPluginInternal(
+ { release: { inject: false }, reactComponentAnnotation: { enabled: true } },
+ buildTool,
+ majorVersion,
+ ) as Plugin;
+
+ await expect(runTransform(plugin, code, '/src/app.jsx')).resolves.toEqual({ code: 'fast-path', map: null });
+
+ expect(createOxcComponentNameAnnotateHooksMock).toHaveBeenCalledWith([], getOxcParseAstAsyncMock);
+ expect(annotationTransformMock).toHaveBeenCalledTimes(1);
+ expect(transformAsyncMock).not.toHaveBeenCalled();
+ });
- expect(viteAnnotationModuleImportMock).not.toHaveBeenCalled();
+ it("uses the fast path with Vite's parser for Vite 8", async () => {
+ const plugin = _rollupPluginInternal(
+ { release: { inject: false }, reactComponentAnnotation: { enabled: true } },
+ 'vite',
+ '8',
+ ) as Plugin;
- const vite8Plugin = _rollupPluginInternal(
- { release: { inject: false }, reactComponentAnnotation: { enabled: true } },
- 'vite',
- '8',
- ) as Plugin;
+ await expect(runTransform(plugin, code, '/src/app.jsx')).resolves.toEqual({ code: 'fast-path', map: null });
+
+ expect(createOxcComponentNameAnnotateHooksMock).toHaveBeenCalledWith([], expect.any(Function));
+ expect(createOxcComponentNameAnnotateHooksMock).not.toHaveBeenCalledWith([], getOxcParseAstAsyncMock);
+ expect(annotationTransformMock).toHaveBeenCalledTimes(1);
+ expect(transformAsyncMock).not.toHaveBeenCalled();
+ });
- await runTransform(vite8Plugin, 'export function App() { return ; }', '/src/app.jsx');
+ it('does not use the fast path when injecting into HTML', async () => {
+ const plugin = _rollupPluginInternal(
+ { release: { inject: false }, reactComponentAnnotation: { enabled: true, _experimentalInjectIntoHtml: true } },
+ 'vite',
+ '8',
+ ) as Plugin;
- expect(viteAnnotationModuleImportMock).toHaveBeenCalledTimes(1);
- expect(viteAnnotationTransformMock).toHaveBeenCalledTimes(1);
+ await runTransform(plugin, code, '/src/app.jsx');
+
+ expect(annotationTransformMock).not.toHaveBeenCalled();
+ expect(transformAsyncMock).toHaveBeenCalledTimes(1);
+ });
});
test('uses a Rollup 3-compatible function transform hook for Rollup builds', () => {
diff --git a/packages/bundler-plugins/test/webpack/public-api.test.ts b/packages/bundler-plugins/test/webpack/public-api.test.ts
index daa1f3e0d87b..97854c6352bf 100644
--- a/packages/bundler-plugins/test/webpack/public-api.test.ts
+++ b/packages/bundler-plugins/test/webpack/public-api.test.ts
@@ -1,12 +1,40 @@
import type { WebpackPluginInstance } from 'webpack';
import { sentryWebpackPlugin } from '../../src/webpack';
-import { describe, it, expect, test } from 'vitest';
+import { describe, it, expect, test, vi } from 'vitest';
test('Webpack plugin should exist', () => {
expect(sentryWebpackPlugin).toBeDefined();
expect(typeof sentryWebpackPlugin).toBe('function');
});
+type PluginClass = new (options: unknown) => unknown;
+
+type Compiler = {
+ options: { plugins: unknown[] };
+ hooks: Record>>;
+ webpack?: { BannerPlugin: PluginClass; DefinePlugin: PluginClass };
+};
+
+class BannerPlugin {
+ public constructor(public options: unknown) {}
+}
+
+class DefinePlugin {
+ public constructor(public options: unknown) {}
+}
+
+function createCompiler(webpack?: Compiler['webpack']): Compiler {
+ return {
+ options: { plugins: [] },
+ hooks: {
+ thisCompilation: { tap: vi.fn() },
+ afterEmit: { tapAsync: vi.fn() },
+ done: { tap: vi.fn() },
+ },
+ webpack,
+ };
+}
+
describe('sentryWebpackPlugin', () => {
it('returns a webpack plugin', () => {
const plugin = sentryWebpackPlugin({
@@ -18,4 +46,25 @@ describe('sentryWebpackPlugin', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
expect(plugin).toEqual({ apply: expect.any(Function) });
});
+
+ it('registers the plugin classes provided by `compiler.webpack`', () => {
+ const compiler = createCompiler({ BannerPlugin, DefinePlugin });
+
+ sentryWebpackPlugin({ telemetry: false, release: { name: 'my-release' } }).apply(compiler);
+
+ expect(compiler.options.plugins).toEqual([expect.any(BannerPlugin)]);
+ });
+
+ it('warns instead of throwing when `compiler.webpack` is unavailable', () => {
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const compiler = createCompiler(undefined);
+
+ expect(() =>
+ sentryWebpackPlugin({ telemetry: false, release: { name: 'my-release' } }).apply(compiler),
+ ).not.toThrow();
+
+ expect(compiler.options.plugins).toEqual([]);
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('BannerPlugin is not available'));
+ warn.mockRestore();
+ });
});
diff --git a/packages/bundler-plugins/test/webpack/webpack5.test.ts b/packages/bundler-plugins/test/webpack/webpack5.test.ts
deleted file mode 100644
index b4d7b0b26f66..000000000000
--- a/packages/bundler-plugins/test/webpack/webpack5.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { WebpackPluginInstance } from 'webpack';
-import { sentryWebpackPlugin } from '../../src/webpack/index';
-import { describe, it, expect, test } from 'vitest';
-
-test('Webpack plugin should exist', () => {
- expect(sentryWebpackPlugin).toBeDefined();
- expect(typeof sentryWebpackPlugin).toBe('function');
-});
-
-describe('sentryWebpackPlugin', () => {
- it('returns a webpack plugin', () => {
- const plugin = sentryWebpackPlugin({
- authToken: 'test-token',
- org: 'test-org',
- project: 'test-project',
- }) as WebpackPluginInstance;
-
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- expect(plugin).toEqual({ apply: expect.any(Function) });
- });
-});
diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts
index b2157e709241..9adc9e5115f0 100644
--- a/packages/cloudflare/src/durableobject.ts
+++ b/packages/cloudflare/src/durableobject.ts
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/unbound-method */
import { RPC } from '@sentry/conventions/op';
-import { isObjectLike } from '@sentry/core';
+import { getDefaultIsolationScope, getIsolationScope, isObjectLike, startNewTrace } from '@sentry/core';
import type { DurableObject } from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels';
import type { CloudflareOptions } from './client';
@@ -144,8 +144,8 @@ function resolveFrameworkManagedMethods(
type RpcInstanceState = {
options: CloudflareOptions;
context: InstrumentedDurableObjectContext;
- /** Per-instance cache of the traced method wrappers, keyed by method name. Created on first use. */
- tracedMethods?: Map;
+ /** Per-instance cache of the instrumented method wrappers, keyed by method name. Created on first use. */
+ instrumentedMethods?: Map;
};
/**
@@ -170,7 +170,7 @@ const RESERVED_RPC_METHOD_NAMES: ReadonlySet = new Set([
const rpcInstanceStates = new WeakMap