diff --git a/CHANGELOG.md b/CHANGELOG.md index e04de0cd43bb..6315e5ba18af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.80.1 + +- fix(astro): Adjust Vite plugin config to upload server source maps (#9541) +- fix(nextjs): Add tracing extensions in all serverside wrappers (#9537) +- fix(nextjs): Fix serverside transaction names on Windows (#9526) +- fix(node): Fix tRPC middleware typing (#9540) +- fix(replay): Add additional safeguards for capturing network bodies (#9506) +- fix(tracing): Update prisma span to be `db.prisma` (#9512) + ## 7.80.0 - feat(astro): Add distributed tracing via `` tags (#9483) @@ -32,7 +41,7 @@ This was possible by extensive use of tree shaking and a host of small changes t By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuration/tree-shaking/) it is possible to shave up to 10 additional KB off the bundle. -#### Other Changes +### Other Changes - feat(astro): Add Sentry middleware (#9445) - feat(feedback): Add "outline focus" and "foreground hover" vars (#9462) @@ -52,13 +61,18 @@ By using [tree shaking](https://docs.sentry.io/platforms/javascript/configuratio ## 7.77.0 +### Security Fixes + +- fix(nextjs): Match only numbers as orgid in tunnelRoute (#9416) (CVE-2023-46729) +- fix(nextjs): Strictly validate tunnel target parameters (#9415) (CVE-2023-46729) + +### Other Changes + - feat: Move LinkedErrors integration to @sentry/core (#9404) - feat(remix): Update sentry-cli version to ^2.21.2 (#9401) - feat(replay): Allow to treeshake & configure compression worker URL (#9409) - fix(angular-ivy): Adjust package entry points to support Angular 17 with SSR config (#9412) - fix(feedback): Fixing feedback import (#9403) -- fix(nextjs): Match only numbers as orgid in tunnelRoute (#9416) -- fix(nextjs): Strictly validate tunnel target parameters (#9415) - fix(utils): Avoid keeping a reference of last used event (#9387) ## 7.76.0 diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 0432fb8108d3..caaf6a073380 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { sentryVitePlugin } from '@sentry/vite-plugin'; -import type { AstroIntegration } from 'astro'; +import type { AstroConfig, AstroIntegration } from 'astro'; import * as fs from 'fs'; import * as path from 'path'; @@ -13,7 +13,8 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { return { name: PKG_NAME, hooks: { - 'astro:config:setup': async ({ updateConfig, injectScript }) => { + // eslint-disable-next-line complexity + 'astro:config:setup': async ({ updateConfig, injectScript, config }) => { // The third param here enables loading of all env vars, regardless of prefix // see: https://main.vitejs.dev/config/#using-environment-variables-in-config @@ -40,6 +41,10 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { project: uploadOptions.project ?? env.SENTRY_PROJECT, authToken: uploadOptions.authToken ?? env.SENTRY_AUTH_TOKEN, telemetry: uploadOptions.telemetry ?? true, + sourcemaps: { + assets: [getSourcemapsAssetsGlob(config)], + }, + debug: options.debug ?? false, }), ], }, @@ -79,3 +84,17 @@ function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined { .map(ext => path.resolve(path.join(process.cwd(), `sentry.${type}.config.${ext}`))) .find(filename => fs.existsSync(filename)); } + +function getSourcemapsAssetsGlob(config: AstroConfig): string { + // paths are stored as "file://" URLs + const outDirPathname = config.outDir && path.resolve(config.outDir.pathname); + const rootDirName = path.resolve((config.root && config.root.pathname) || process.cwd()); + + if (outDirPathname) { + const relativePath = path.relative(rootDirName, outDirPathname); + return `${relativePath}/**/*`; + } + + // fallback to default output dir + return 'dist/**/*'; +} diff --git a/packages/astro/test/integration/index.test.ts b/packages/astro/test/integration/index.test.ts index fe876023c9c4..2f98fc991065 100644 --- a/packages/astro/test/integration/index.test.ts +++ b/packages/astro/test/integration/index.test.ts @@ -14,6 +14,13 @@ process.env = { SENTRY_AUTH_TOKEN: 'my-token', }; +const updateConfig = vi.fn(); +const injectScript = vi.fn(); +const config = { + root: new URL('file://path/to/project'), + outDir: new URL('file://path/to/project/out'), +}; + describe('sentryAstro integration', () => { afterEach(() => { vi.clearAllMocks(); @@ -28,12 +35,10 @@ describe('sentryAstro integration', () => { const integration = sentryAstro({ sourceMapsUploadOptions: { enabled: true, org: 'my-org', project: 'my-project', telemetry: false }, }); - const updateConfig = vi.fn(); - const injectScript = vi.fn(); expect(integration.hooks['astro:config:setup']).toBeDefined(); // @ts-expect-error - the hook exists and we only need to pass what we actually use - await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config }); expect(updateConfig).toHaveBeenCalledTimes(1); expect(updateConfig).toHaveBeenCalledWith({ @@ -51,6 +56,30 @@ describe('sentryAstro integration', () => { org: 'my-org', project: 'my-project', telemetry: false, + debug: false, + sourcemaps: { + assets: ['out/**/*'], + }, + }); + }); + + it('falls back to default output dir, if out and root dir are not available', async () => { + const integration = sentryAstro({ + sourceMapsUploadOptions: { enabled: true, org: 'my-org', project: 'my-project', telemetry: false }, + }); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config: {} }); + + expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1); + expect(sentryVitePluginSpy).toHaveBeenCalledWith({ + authToken: 'my-token', + org: 'my-org', + project: 'my-project', + telemetry: false, + debug: false, + sourcemaps: { + assets: ['dist/**/*'], + }, }); }); @@ -58,12 +87,10 @@ describe('sentryAstro integration', () => { const integration = sentryAstro({ sourceMapsUploadOptions: { enabled: false }, }); - const updateConfig = vi.fn(); - const injectScript = vi.fn(); expect(integration.hooks['astro:config:setup']).toBeDefined(); // @ts-expect-error - the hook exists and we only need to pass what we actually use - await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config }); expect(updateConfig).toHaveBeenCalledTimes(0); expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0); @@ -71,12 +98,10 @@ describe('sentryAstro integration', () => { it('injects client and server init scripts', async () => { const integration = sentryAstro({}); - const updateConfig = vi.fn(); - const injectScript = vi.fn(); expect(integration.hooks['astro:config:setup']).toBeDefined(); // @ts-expect-error - the hook exists and we only need to pass what we actually use - await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config }); expect(injectScript).toHaveBeenCalledTimes(2); expect(injectScript).toHaveBeenCalledWith('page', expect.stringContaining('Sentry.init')); @@ -89,12 +114,9 @@ describe('sentryAstro integration', () => { serverInitPath: 'my-server-init-path.js', }); - const updateConfig = vi.fn(); - const injectScript = vi.fn(); - expect(integration.hooks['astro:config:setup']).toBeDefined(); // @ts-expect-error - the hook exists and we only need to pass what we actually use - await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config }); expect(injectScript).toHaveBeenCalledTimes(2); expect(injectScript).toHaveBeenCalledWith('page', expect.stringContaining('my-client-init-path.js')); diff --git a/packages/browser-integration-tests/package.json b/packages/browser-integration-tests/package.json index 7b891b36a844..2de91dc7e8b9 100644 --- a/packages/browser-integration-tests/package.json +++ b/packages/browser-integration-tests/package.json @@ -49,7 +49,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.31.1", - "axios": "1.3.4", + "axios": "1.6.0", "babel-loader": "^8.2.2", "html-webpack-plugin": "^5.5.0", "pako": "^2.1.0", diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts index bd8f0e9270c6..88a1c89fba0d 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts @@ -33,9 +33,7 @@ sentryTest('captures Breadcrumb for clicks & debounces them for a second', async await page.waitForTimeout(1000); await page.click('#button2'); - await page.evaluate('Sentry.captureException("test exception")'); - - const eventData = await promise; + const [eventData] = await Promise.all([promise, page.evaluate('Sentry.captureException("test exception")')]); expect(eventData.exception?.values).toHaveLength(1); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts index 08fcf0a25446..7158f034a2ef 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts @@ -54,7 +54,7 @@ sentryTest('captures request headers', async ({ getLocalTestPath, page, browserN /* eslint-enable */ }); - const request = await requestPromise; + const [request, replayReq1] = await Promise.all([requestPromise, replayRequestPromise1]); const eventData = envelopeRequestParser(request); expect(eventData.exception?.values).toHaveLength(1); @@ -71,7 +71,6 @@ sentryTest('captures request headers', async ({ getLocalTestPath, page, browserN }, }); - const replayReq1 = await replayRequestPromise1; const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ { @@ -142,7 +141,8 @@ sentryTest( /* eslint-enable */ }); - const request = await requestPromise; + const [request, replayReq1] = await Promise.all([requestPromise, replayRequestPromise1]); + const eventData = envelopeRequestParser(request); expect(eventData.exception?.values).toHaveLength(1); @@ -159,7 +159,6 @@ sentryTest( }, }); - const replayReq1 = await replayRequestPromise1; const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1); expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([ { diff --git a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts index e98839f7fa37..88bc531017d9 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts +++ b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -57,22 +57,24 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - const fullSnapshot = getFullRecordingSnapshots(await reqPromise0); + + const text = 'test'; + + const [req0] = await Promise.all([reqPromise0, page.locator('#input').fill(text)]); + await forceFlushReplay(); + + const fullSnapshot = getFullRecordingSnapshots(req0); const stringifiedSnapshot = JSON.stringify(fullSnapshot); expect(stringifiedSnapshot.includes('Submit form')).toBe(false); expect(stringifiedSnapshot.includes('Unmasked button')).toBe(true); - const text = 'test'; - - await page.locator('#input').fill(text); + const [req1] = await Promise.all([reqPromise1, page.locator('#input-unmasked').fill(text)]); await forceFlushReplay(); - const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); + const snapshots = getIncrementalRecordingSnapshots(req1).filter(isInputMutation); const lastSnapshot = snapshots[snapshots.length - 1]; expect(lastSnapshot.data.text).toBe('*'.repeat(text.length)); - await page.locator('#input-unmasked').fill(text); - await forceFlushReplay(); const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); const lastSnapshot2 = snapshots2[snapshots2.length - 1]; expect(lastSnapshot2.data.text).toBe(text); @@ -120,18 +122,18 @@ sentryTest( await page.goto(url); - await reqPromise0; - const text = 'test'; - await page.locator('#textarea').fill(text); + await Promise.all([reqPromise0, page.locator('#textarea').fill(text)]); + await forceFlushReplay(); + + const [req1] = await Promise.all([reqPromise1, page.locator('#textarea-unmasked').fill(text)]); await forceFlushReplay(); - const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); + + const snapshots = getIncrementalRecordingSnapshots(req1).filter(isInputMutation); const lastSnapshot = snapshots[snapshots.length - 1]; expect(lastSnapshot.data.text).toBe('*'.repeat(text.length)); - await page.locator('#textarea-unmasked').fill(text); - await forceFlushReplay(); const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); const lastSnapshot2 = snapshots2[snapshots2.length - 1]; expect(lastSnapshot2.data.text).toBe(text); diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/package.json b/packages/e2e-tests/test-applications/react-create-hash-router/package.json index 2ea0b4077e18..d1842bc24d91 100644 --- a/packages/e2e-tests/test-applications/react-create-hash-router/package.json +++ b/packages/e2e-tests/test-applications/react-create-hash-router/package.json @@ -47,7 +47,7 @@ }, "devDependencies": { "@playwright/test": "1.26.1", - "axios": "1.1.2", + "axios": "1.6.0", "serve": "14.0.1" }, "volta": { diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/package.json b/packages/e2e-tests/test-applications/react-router-6-use-routes/package.json index e287bb9c8873..a0e7c162f0f0 100644 --- a/packages/e2e-tests/test-applications/react-router-6-use-routes/package.json +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/package.json @@ -48,7 +48,7 @@ }, "devDependencies": { "@playwright/test": "1.26.1", - "axios": "1.1.2", + "axios": "1.6.0", "serve": "14.0.1" }, "volta": { diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json index bb5b06eb0378..be15956c2ca6 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json @@ -47,7 +47,7 @@ }, "devDependencies": { "@playwright/test": "1.26.1", - "axios": "1.1.2", + "axios": "1.6.0", "serve": "14.0.1" }, "volta": { diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/package.json b/packages/e2e-tests/test-applications/standard-frontend-react/package.json index ab9f06cb3ca0..10ace7b83893 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/package.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react/package.json @@ -48,7 +48,7 @@ }, "devDependencies": { "@playwright/test": "1.26.1", - "axios": "1.1.2", + "axios": "1.6.0", "serve": "14.0.1" }, "volta": { diff --git a/packages/feedback/src/sendFeedback.ts b/packages/feedback/src/sendFeedback.ts index e149a290e82a..c77013602d6b 100644 --- a/packages/feedback/src/sendFeedback.ts +++ b/packages/feedback/src/sendFeedback.ts @@ -10,13 +10,14 @@ interface SendFeedbackParams { name?: string; email?: string; url?: string; + source?: string; } /** * Public API to send a Feedback item to Sentry */ export function sendFeedback( - { name, email, message, url = getLocationHref() }: SendFeedbackParams, + { name, email, message, source = 'api', url = getLocationHref() }: SendFeedbackParams, { includeReplay = true }: SendFeedbackOptions = {}, ): ReturnType { const client = getCurrentHub().getClient(); @@ -37,6 +38,7 @@ export function sendFeedback( message, url, replay_id: replayId, + source, }, }); } diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts index 6269e8a697a8..5772e6e8176b 100644 --- a/packages/feedback/src/types/index.ts +++ b/packages/feedback/src/types/index.ts @@ -12,6 +12,7 @@ export interface SendFeedbackData { email?: string; replay_id?: string; name?: string; + source?: string; }; } diff --git a/packages/feedback/src/util/handleFeedbackSubmit.ts b/packages/feedback/src/util/handleFeedbackSubmit.ts index 6e7b7c014de4..4e8f233b15fd 100644 --- a/packages/feedback/src/util/handleFeedbackSubmit.ts +++ b/packages/feedback/src/util/handleFeedbackSubmit.ts @@ -29,7 +29,7 @@ export async function handleFeedbackSubmit( dialog.hideError(); try { - const resp = await sendFeedback(feedback, options); + const resp = await sendFeedback({ ...feedback, source: 'widget' }, options); // Success! return resp; diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts index 45a3bd493de9..d82375ded4d2 100644 --- a/packages/feedback/src/util/sendFeedbackRequest.ts +++ b/packages/feedback/src/util/sendFeedbackRequest.ts @@ -8,7 +8,7 @@ import { prepareFeedbackEvent } from './prepareFeedbackEvent'; * Send feedback using transport */ export async function sendFeedbackRequest({ - feedback: { message, email, name, replay_id, url }, + feedback: { message, email, name, source, replay_id, url }, }: SendFeedbackData): Promise { const hub = getCurrentHub(); const client = hub.getClient(); @@ -28,6 +28,7 @@ export async function sendFeedbackRequest({ message, replay_id, url, + source, }, }, type: 'feedback', diff --git a/packages/feedback/test/sendFeedback.test.ts b/packages/feedback/test/sendFeedback.test.ts new file mode 100644 index 000000000000..33474e2df673 --- /dev/null +++ b/packages/feedback/test/sendFeedback.test.ts @@ -0,0 +1,43 @@ +import { getCurrentHub } from '@sentry/core'; + +import { sendFeedback } from '../src/sendFeedback'; +import { mockSdk } from './utils/mockSdk'; + +describe('sendFeedback', () => { + it('sends feedback', async () => { + mockSdk(); + const mockTransport = jest.spyOn(getCurrentHub().getClient()!.getTransport()!, 'send'); + + await sendFeedback({ + name: 'doe', + email: 're@example.org', + message: 'mi', + }); + expect(mockTransport).toHaveBeenCalledWith([ + { event_id: expect.any(String), sent_at: expect.any(String) }, + [ + [ + { type: 'feedback' }, + { + breadcrumbs: undefined, + contexts: { + feedback: { + contact_email: 're@example.org', + message: 'mi', + name: 'doe', + replay_id: undefined, + source: 'api', + url: 'http://localhost/', + }, + }, + environment: 'production', + event_id: expect.any(String), + platform: 'javascript', + timestamp: expect.any(Number), + type: 'feedback', + }, + ], + ], + ]); + }); +}); diff --git a/packages/feedback/test/utils/mockSdk.ts b/packages/feedback/test/utils/mockSdk.ts new file mode 100644 index 000000000000..0dbc30c02cb6 --- /dev/null +++ b/packages/feedback/test/utils/mockSdk.ts @@ -0,0 +1,58 @@ +import type { Envelope, Transport, TransportMakeRequestResponse } from '@sentry/types'; + +import type { TestClientOptions } from '../utils/TestClient'; +import { getDefaultClientOptions, init } from '../utils/TestClient'; + +export interface MockSdkParams { + sentryOptions?: Partial; +} + +class MockTransport implements Transport { + send: (request: Envelope) => PromiseLike; + + constructor() { + const send: ((request: Envelope) => PromiseLike) & { + __sentry__baseTransport__?: boolean; + } = jest.fn(async () => { + return { + statusCode: 200, + }; + }); + + send.__sentry__baseTransport__ = true; + this.send = send; + } + + async flush() { + return true; + } + async sendEvent(_e: Event) { + return { + status: 'skipped', + event: 'ok', + type: 'transaction', + }; + } + async sendSession() { + return; + } + async recordLostEvent() { + return; + } + async close() { + return; + } +} + +export async function mockSdk({ sentryOptions }: MockSdkParams = {}): Promise { + init({ + ...getDefaultClientOptions(), + dsn: 'https://dsn@ingest.f00.f00/1', + autoSessionTracking: false, + sendClientReports: false, + transport: () => new MockTransport(), + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 0.0, + ...sentryOptions, + }); +} diff --git a/packages/feedback/test/widget/createWidget.test.ts b/packages/feedback/test/widget/createWidget.test.ts index 1c39de8f26c1..fb1809de7ecf 100644 --- a/packages/feedback/test/widget/createWidget.test.ts +++ b/packages/feedback/test/widget/createWidget.test.ts @@ -155,6 +155,7 @@ describe('createWidget', () => { message: 'My feedback', url: 'http://localhost/', replay_id: undefined, + source: 'widget', }, }); @@ -194,6 +195,7 @@ describe('createWidget', () => { message: 'My feedback', url: 'http://localhost/', replay_id: undefined, + source: 'widget', }, }); diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 26239f3db2ee..df169a6f7e2d 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -1,4 +1,4 @@ -import { captureException, flush, getCurrentHub, startTransaction } from '@sentry/core'; +import { addTracingExtensions, captureException, flush, getCurrentHub, startTransaction } from '@sentry/core'; import type { Span } from '@sentry/types'; import { addExceptionMechanism, logger, objectify, tracingContextFromHeaders } from '@sentry/utils'; @@ -12,6 +12,8 @@ export function withEdgeWrapping( options: { spanDescription: string; spanOp: string; mechanismFunctionName: string }, ): (...params: Parameters) => Promise> { return async function (this: unknown, ...args) { + addTracingExtensions(); + const req = args[0]; const currentScope = getCurrentHub().getScope(); const prevSpan = currentScope.getSpan(); diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index fdc26537607d..5a93a257a209 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -1,4 +1,10 @@ -import { captureException, getCurrentHub, runWithAsyncContext, startTransaction } from '@sentry/core'; +import { + addTracingExtensions, + captureException, + getCurrentHub, + runWithAsyncContext, + startTransaction, +} from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { addExceptionMechanism, @@ -74,6 +80,8 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri } req.__withSentry_applied__ = true; + addTracingExtensions(); + // eslint-disable-next-line complexity, @typescript-eslint/no-explicit-any const boundHandler = runWithAsyncContext( // eslint-disable-next-line complexity diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts index 43672a1acf28..ae7bb19be43e 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts @@ -1,4 +1,4 @@ -import { captureCheckIn, runWithAsyncContext } from '@sentry/core'; +import { addTracingExtensions, captureCheckIn, runWithAsyncContext } from '@sentry/core'; import type { NextApiRequest } from 'next'; import type { VercelCronsConfig } from './types'; @@ -24,6 +24,7 @@ export function wrapApiHandlerWithSentryVercelCrons any>( routeHandler: F, context: RouteHandlerContext, ): (...args: Parameters) => ReturnType extends Promise ? ReturnType : Promise> { + addTracingExtensions(); const { method, parameterizedRoute, baggageHeader, sentryTraceHeader } = context; - return new Proxy(routeHandler, { apply: (originalFunction, thisArg, args) => { return runWithAsyncContext(async () => { diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 6efa50d2b804..e909bd114c7c 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,4 +1,11 @@ -import { captureException, flush, getCurrentHub, runWithAsyncContext, startTransaction } from '@sentry/core'; +import { + addTracingExtensions, + captureException, + flush, + getCurrentHub, + runWithAsyncContext, + startTransaction, +} from '@sentry/core'; import { addExceptionMechanism, tracingContextFromHeaders } from '@sentry/utils'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; @@ -12,6 +19,7 @@ export function wrapServerComponentWithSentry any> appDirComponent: F, context: ServerComponentContext, ): F { + addTracingExtensions(); const { componentRoute, componentType } = context; // Even though users may define server components as async functions, for the client bundles diff --git a/packages/nextjs/src/config/loaders/types.ts b/packages/nextjs/src/config/loaders/types.ts index ef3a017be0c7..8e03a57ade36 100644 --- a/packages/nextjs/src/config/loaders/types.ts +++ b/packages/nextjs/src/config/loaders/types.ts @@ -8,13 +8,6 @@ export type LoaderThis = { */ resourcePath: string; - /** - * Query at the end of resolved file name ("../some-folder/some-module?foobar" -> resourceQuery: "?foobar") - * - * https://webpack.js.org/api/loaders/#thisresourcequery - */ - resourceQuery: string; - /** * Function to add outside file used by loader to `watch` process * diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index 346f56a91cbb..ed2680467a47 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -40,7 +40,7 @@ const serverComponentWrapperTemplateCode = fs.readFileSync(serverComponentWrappe const routeHandlerWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'routeHandlerWrapperTemplate.js'); const routeHandlerWrapperTemplateCode = fs.readFileSync(routeHandlerWrapperTemplatePath, { encoding: 'utf8' }); -type LoaderOptions = { +export type WrappingLoaderOptions = { pagesDir: string; appDir: string; pageExtensionRegex: string; @@ -58,7 +58,7 @@ type LoaderOptions = { */ // eslint-disable-next-line complexity export default function wrappingLoader( - this: LoaderThis, + this: LoaderThis, userCode: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any userModuleSourceMap: any, @@ -102,12 +102,11 @@ export default function wrappingLoader( } } else if (wrappingTargetKind === 'page' || wrappingTargetKind === 'api-route') { // Get the parameterized route name from this page's filepath - const parameterizedPagesRoute = path.posix - .normalize( - path - // Get the path of the file insde of the pages directory - .relative(pagesDir, this.resourcePath), - ) + const parameterizedPagesRoute = path + // Get the path of the file insde of the pages directory + .relative(pagesDir, this.resourcePath) + // Replace all backslashes with forward slashes (windows) + .replace(/\\/g, '/') // Add a slash at the beginning .replace(/(.*)/, '/$1') // Pull off the file extension @@ -139,8 +138,11 @@ export default function wrappingLoader( templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\')); } else if (wrappingTargetKind === 'server-component' || wrappingTargetKind === 'route-handler') { // Get the parameterized route name from this page's filepath - const parameterizedPagesRoute = path.posix - .normalize(path.relative(appDir, this.resourcePath)) + const parameterizedPagesRoute = path + // Get the path of the file insde of the app directory + .relative(appDir, this.resourcePath) + // Replace all backslashes with forward slashes (windows) + .replace(/\\/g, '/') // Add a slash at the beginning .replace(/(.*)/, '/$1') // Pull off the file name diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 1ff0fbf9a6db..c3ff833a4e4c 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -35,6 +35,7 @@ const RUNTIME_TO_SDK_ENTRYPOINT_MAP = { let showedMissingAuthTokenErrorMsg = false; let showedMissingOrgSlugErrorMsg = false; let showedMissingProjectSlugErrorMsg = false; +let showedMissingCLiBinaryErrorMsg = false; let showedHiddenSourceMapsWarningMsg = false; // TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile @@ -844,10 +845,15 @@ function shouldEnableWebpackPlugin(buildContext: BuildContext, userSentryOptions // @ts-expect-error - this exists, the dynamic import just doesn't know it if (!SentryWebpackPlugin || !SentryWebpackPlugin.cliBinaryExists()) { - // eslint-disable-next-line no-console - console.error( - `${chalk.red('error')} - ${chalk.bold('Sentry CLI binary not found.')} Source maps will not be uploaded.\n`, - ); + if (!showedMissingCLiBinaryErrorMsg) { + // eslint-disable-next-line no-console + console.error( + `${chalk.red('error')} - ${chalk.bold( + 'Sentry CLI binary not found.', + )} Source maps will not be uploaded. Please check that postinstall scripts are enabled in your package manager when installing your dependencies and please run your build once without any caching to avoid caching issues of dependencies.\n`, + ); + showedMissingCLiBinaryErrorMsg = true; + } return false; } diff --git a/packages/nextjs/test/buildProcess/testApp/yarn.lock b/packages/nextjs/test/buildProcess/testApp/yarn.lock index cbe67329d3d6..79446821003e 100644 --- a/packages/nextjs/test/buildProcess/testApp/yarn.lock +++ b/packages/nextjs/test/buildProcess/testApp/yarn.lock @@ -113,27 +113,25 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@sentry-internal/tracing@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.57.0.tgz#cb761931b635f8f24c84be0eecfacb8516b20551" - integrity sha512-tpViyDd8AhQGYYhI94xi2aaDopXOPfL2Apwrtb3qirWkomIQ2K86W1mPmkce+B0cFOnW2Dxv/ZTFKz6ghjK75A== - dependencies: - "@sentry/core" "7.57.0" - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" - tslib "^2.4.1 || ^1.9.3" - -"@sentry/browser@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.57.0.tgz#6e724c9eac680dba99ced0fdf81be8d1e3b3bceb" - integrity sha512-E0HaYYlaqHFiIRZXxcvOO8Odvlt+TR1vFFXzqUWXPOvDRxURglTOCQ3EN/u6bxtAGJ6y/Zc2obgihTtypuel/w== - dependencies: - "@sentry-internal/tracing" "7.57.0" - "@sentry/core" "7.57.0" - "@sentry/replay" "7.57.0" - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" - tslib "^2.4.1 || ^1.9.3" +"@sentry-internal/tracing@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.80.0.tgz#f9a6c0456b3cbf4a53c986a0b9208572d80e0756" + integrity sha512-P1Ab9gamHLsbH9D82i1HY8xfq9dP8runvc4g50AAd6OXRKaJ45f2KGRZUmnMEVqBQ7YoPYp2LFMkrhNYbcZEoQ== + dependencies: + "@sentry/core" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" + +"@sentry/browser@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.80.0.tgz#385fb59ac1d52b67919087f3d7044575ae0abbdd" + integrity sha512-Ngwjc+yyf/aH5q7iQM1LeDNlhM1Ilt4ZLUogTghZR/guwNWmCtk3OHcjOLz7fxBBj9wGFUc2pHPyeYM6bQhrEw== + dependencies: + "@sentry-internal/tracing" "7.80.0" + "@sentry/core" "7.80.0" + "@sentry/replay" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" "@sentry/cli@^1.74.6": version "1.74.6" @@ -148,87 +146,92 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.57.0.tgz#65093d739c04f320a54395a21be955fcbe326acb" - integrity sha512-l014NudPH0vQlzybtXajPxYFfs9w762NoarjObC3gu76D1jzBBFzhdRelkGpDbSLNTIsKhEDDRpgAjBWJ9icfw== +"@sentry/core@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.80.0.tgz#7b8a460c19160b81ade20080333189f1a80c1410" + integrity sha512-nJiiymdTSEyI035/rdD3VOq6FlOZ2wWLR5bit9LK8a3rzHU3UXkwScvEo6zYgs0Xp1sC0yu1S9+0BEiYkmi29A== dependencies: - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" - tslib "^2.4.1 || ^1.9.3" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" -"@sentry/integrations@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.57.0.tgz#298085b3a2fe862cc70bc7f2143aa0fbc617322c" - integrity sha512-C3WZo5AGI2L0dj+mIjeZpdAwDEG2nDYvZbTzq5J9hVoHFdP3t7fOWBHSPkSFVtTdMaJrv+82aKnUefVCeAjxGg== +"@sentry/integrations@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.80.0.tgz#d81dc3b357d4efd4368471b39496494ab221a64a" + integrity sha512-9xI+jtqSBrAG/Y2f4OyeJhl6WZR3i0qCXRwqCZoCFCDgN4ZQORc4VBwaC3nW2s9jgfb13FC2FQToGOVrRnsetg== dependencies: - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" + "@sentry/core" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" localforage "^1.8.1" - tslib "^2.4.1 || ^1.9.3" "@sentry/nextjs@file:../../..": - version "7.57.0" + version "7.80.0" dependencies: "@rollup/plugin-commonjs" "24.0.0" - "@sentry/core" "7.57.0" - "@sentry/integrations" "7.57.0" - "@sentry/node" "7.57.0" - "@sentry/react" "7.57.0" - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" + "@sentry/core" "7.80.0" + "@sentry/integrations" "7.80.0" + "@sentry/node" "7.80.0" + "@sentry/react" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" + "@sentry/vercel-edge" "7.80.0" "@sentry/webpack-plugin" "1.20.0" chalk "3.0.0" + resolve "1.22.8" rollup "2.78.0" stacktrace-parser "^0.1.10" - tslib "^2.4.1 || ^1.9.3" - -"@sentry/node@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.57.0.tgz#31052f5988ed4496d7f3ff925240cf9b02d09941" - integrity sha512-63mjyUVM6sfJFVQ5TGVRVGUsoEfESl5ABzIW1W0s9gUiQPaG8SOdaQJglb2VNrkMYxnRHgD8Q9LUh/qcmUyPGw== - dependencies: - "@sentry-internal/tracing" "7.57.0" - "@sentry/core" "7.57.0" - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" - cookie "^0.4.1" + +"@sentry/node@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.80.0.tgz#7e060bc934a58a442b786246d46a5a0dd822ae44" + integrity sha512-J35fqe8J5ac/17ZXT0ML3opYGTOclqYNE9Sybs1y9n6BqacHyzH8By72YrdI03F7JJDHwrcGw+/H8hGpkCwi0Q== + dependencies: + "@sentry-internal/tracing" "7.80.0" + "@sentry/core" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^2.4.1 || ^1.9.3" -"@sentry/react@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.57.0.tgz#cf91f0115bcd2a8306d6c8a39d8e8b53d4b21814" - integrity sha512-XGNTjIoCG3naSmCU8qObd+y+CqAB6NQkGWOp2yyBwp2inyKF2ehJvDh6bIQloBYq2TmOJDa4NfXdMrkilxaLFQ== +"@sentry/react@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.80.0.tgz#ee589ff202174ced45e77dc2714237031ca9c726" + integrity sha512-xoX7fqgY0NZR9Fud/IJ4a3b8Z/HsdwU5SLILi46lV+CWaXS6eFM1E81jG2Vd2EeYIpkH+bMA//XHMEod8LAJcQ== dependencies: - "@sentry/browser" "7.57.0" - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" + "@sentry/browser" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" hoist-non-react-statics "^3.3.2" - tslib "^2.4.1 || ^1.9.3" -"@sentry/replay@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.57.0.tgz#c8f7eae7b7edc9d32c3d2955b337f3b3c76dff39" - integrity sha512-pN4ryNS3J5EYbkXvR+O/+hseAJha7XDl8mPFtK0OGTHG10JzCi4tQJazblHQdpb5QBaMMPCeZ+isyfoQLDNXnw== +"@sentry/replay@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.80.0.tgz#0626d85af1d8573038d52ae9e244e3e95fa47385" + integrity sha512-wWnpuJq3OaDLp1LutE4oxWXnau04fvwuzBjuaFvOXOV+pB/kn+pDPuVOC5+FH/RMRZ5ftwX5+dF6fojfcLVGCg== dependencies: - "@sentry/core" "7.57.0" - "@sentry/types" "7.57.0" - "@sentry/utils" "7.57.0" + "@sentry-internal/tracing" "7.80.0" + "@sentry/core" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" -"@sentry/types@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.57.0.tgz#4fdb80cbd49ba034dd8d9be0c0005a016d5db3ce" - integrity sha512-D7ifoUfxuVCUyktIr5Gc+jXUbtcUMmfHdTtTbf1XCZHua5mJceK9wtl3YCg3eq/HK2Ppd52BKnTzEcS5ZKQM+w== +"@sentry/types@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.80.0.tgz#f6896de2d231a7f8d814cf1c981c474240e96d8a" + integrity sha512-4bpMO+2jWiWLDa8zbTASWWNLWe6yhjfPsa7/6VH5y9x1NGtL8oRbqUsTgsvjF3nmeHEMkHQsC8NHPaQ/ibFmZQ== -"@sentry/utils@7.57.0": - version "7.57.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.57.0.tgz#8253c6fcf35138b4c424234b8da1596e11b98ad8" - integrity sha512-YXrkMCiNklqkXctn4mKYkrzNCf/dfVcRUQrkXjeBC+PHXbcpPyaJgInNvztR7Skl8lE3JPGPN4v5XhLxK1bUUg== +"@sentry/utils@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.80.0.tgz#5bd682fa9a382eea952d4fa3628f0f33e4240ff3" + integrity sha512-XbBCEl6uLvE50ftKwrEo6XWdDaZXHXu+kkHXTPWQEcnbvfZKLuG9V0Hxtxxq3xQgyWmuF05OH1GcqYqiO+v5Yg== dependencies: - "@sentry/types" "7.57.0" - tslib "^2.4.1 || ^1.9.3" + "@sentry/types" "7.80.0" + +"@sentry/vercel-edge@7.80.0": + version "7.80.0" + resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.80.0.tgz#674f77e820db066f408d4aab7887f964361706bb" + integrity sha512-Jh7Kg1+zrSbOqPcLmMQGaGWE2ieJcaCVrvuRgVxUCinZlHB2r5RUlXKLqR6GXV+LVqv8NQDIv1wrKfLSHdSKJA== + dependencies: + "@sentry/core" "7.80.0" + "@sentry/types" "7.80.0" + "@sentry/utils" "7.80.0" "@sentry/webpack-plugin@1.20.0": version "1.20.0" @@ -426,11 +429,6 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control- resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -cookie@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -497,6 +495,11 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gauge@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" @@ -564,6 +567,13 @@ has-unicode@^2.0.0, has-unicode@^2.0.1: resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -597,6 +607,13 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -664,11 +681,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== - magic-string@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" @@ -834,6 +846,11 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -915,6 +932,15 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve@1.22.8: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -1047,6 +1073,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + tar@^6.1.11: version "6.1.12" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" @@ -1076,11 +1107,6 @@ tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== -"tslib@^2.4.1 || ^1.9.3": - version "2.6.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" - integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== - type-fest@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" diff --git a/packages/nextjs/test/config/wrappingLoader.test.ts b/packages/nextjs/test/config/wrappingLoader.test.ts new file mode 100644 index 000000000000..2c05b4bcdbff --- /dev/null +++ b/packages/nextjs/test/config/wrappingLoader.test.ts @@ -0,0 +1,105 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const originalReadfileSync = fs.readFileSync; + +jest.spyOn(fs, 'readFileSync').mockImplementation((filePath, options) => { + if (filePath.toString().endsWith('/config/templates/apiWrapperTemplate.js')) { + return originalReadfileSync( + path.join(__dirname, '../../build/cjs/config/templates/apiWrapperTemplate.js'), + options, + ); + } + + if (filePath.toString().endsWith('/config/templates/pageWrapperTemplate.js')) { + return originalReadfileSync( + path.join(__dirname, '../../build/cjs/config/templates/pageWrapperTemplate.js'), + options, + ); + } + + if (filePath.toString().endsWith('/config/templates/middlewareWrapperTemplate.js')) { + return originalReadfileSync( + path.join(__dirname, '../../build/cjs/config/templates/middlewareWrapperTemplate.js'), + options, + ); + } + + if (filePath.toString().endsWith('/config/templates/sentryInitWrapperTemplate.js')) { + return originalReadfileSync( + path.join(__dirname, '../../build/cjs/config/templates/sentryInitWrapperTemplate.js'), + options, + ); + } + + if (filePath.toString().endsWith('/config/templates/serverComponentWrapperTemplate.js')) { + return originalReadfileSync( + path.join(__dirname, '../../build/cjs/config/templates/serverComponentWrapperTemplate.js'), + options, + ); + } + + if (filePath.toString().endsWith('/config/templates/routeHandlerWrapperTemplate.js')) { + return originalReadfileSync( + path.join(__dirname, '../../build/cjs/config/templates/routeHandlerWrapperTemplate.js'), + options, + ); + } + + return originalReadfileSync(filePath, options); +}); + +import type { LoaderThis } from '../../src/config/loaders/types'; +import type { WrappingLoaderOptions } from '../../src/config/loaders/wrappingLoader'; +import wrappingLoader from '../../src/config/loaders/wrappingLoader'; + +const DEFAULT_PAGE_EXTENSION_REGEX = ['tsx', 'ts', 'jsx', 'js'].join('|'); + +const defaultLoaderThis = { + addDependency: () => undefined, + async: () => undefined, + cacheable: () => undefined, +}; + +describe('wrappingLoader', () => { + it('should correctly wrap API routes on unix', async () => { + const callback = jest.fn(); + + const userCode = ` + export default function handler(req, res) { + res.json({ foo: "bar" }); + } + `; + const userCodeSourceMap = undefined; + + const loaderPromise = new Promise(resolve => { + const loaderThis = { + ...defaultLoaderThis, + resourcePath: '/my/pages/my/route.ts', + callback: callback.mockImplementation(() => { + resolve(); + }), + getOptions() { + return { + pagesDir: '/my/pages', + appDir: '/my/app', + pageExtensionRegex: DEFAULT_PAGE_EXTENSION_REGEX, + excludeServerRoutes: [], + wrappingTargetKind: 'api-route', + sentryConfigFilePath: '/my/sentry.server.config.ts', + vercelCronsConfig: undefined, + nextjsRequestAsyncStorageModulePath: '/my/request-async-storage.js', + }; + }, + } satisfies LoaderThis; + + wrappingLoader.call(loaderThis, userCode, userCodeSourceMap); + }); + + await loaderPromise; + + expect(callback).toHaveBeenCalledWith(null, expect.stringContaining("'/my/route'"), expect.anything()); + }); + + it.todo('should correctly wrap API routes on unix'); +}); diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts index 040f3890fc11..cae5bbc2c46a 100644 --- a/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts +++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts @@ -10,17 +10,17 @@ conditionalTest({ min: 12 })('Prisma ORM Integration', () => { spans: [ { description: 'User create', - op: 'db.sql.prisma', + op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.operation': 'create', 'db.prisma.version': '3.12.0' }, }, { description: 'User findMany', - op: 'db.sql.prisma', + op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.operation': 'findMany', 'db.prisma.version': '3.12.0' }, }, { description: 'User deleteMany', - op: 'db.sql.prisma', + op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.operation': 'deleteMany', 'db.prisma.version': '3.12.0' }, }, ], diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts b/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts index 040f3890fc11..cae5bbc2c46a 100644 --- a/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts @@ -10,17 +10,17 @@ conditionalTest({ min: 12 })('Prisma ORM Integration', () => { spans: [ { description: 'User create', - op: 'db.sql.prisma', + op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.operation': 'create', 'db.prisma.version': '3.12.0' }, }, { description: 'User findMany', - op: 'db.sql.prisma', + op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.operation': 'findMany', 'db.prisma.version': '3.12.0' }, }, { description: 'User deleteMany', - op: 'db.sql.prisma', + op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.operation': 'deleteMany', 'db.prisma.version': '3.12.0' }, }, ], diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index f080b00275dc..ab6dd6325ce6 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -16,6 +16,7 @@ import { dropUndefinedKeys, extractPathForTransaction, isString, + isThenable, logger, normalize, tracingContextFromHeaders, @@ -326,7 +327,7 @@ interface SentryTrpcMiddlewareOptions { interface TrpcMiddlewareArguments { path: string; - type: 'query' | 'mutation' | 'subscription'; + type: string; next: () => T; rawInput: unknown; } @@ -338,7 +339,7 @@ interface TrpcMiddlewareArguments { * e.g. Express Request Handlers or Next.js SDK. */ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { - return async function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): Promise { + return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T { const hub = getCurrentHub(); const clientOptions = hub.getClient()?.getOptions(); const sentryTransaction = hub.getScope().getTransaction(); @@ -358,36 +359,49 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { sentryTransaction.setContext('trpc', trpcContext); } - function captureError(e: unknown): void { - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, + function shouldCaptureError(e: unknown): boolean { + if (typeof e === 'object' && e && 'code' in e) { + // Is likely TRPCError - we only want to capture internal server errors + return e.code === 'INTERNAL_SERVER_ERROR'; + } else { + // Is likely random error that bubbles up + return true; + } + } + + function handleErrorCase(e: unknown): void { + if (shouldCaptureError(e)) { + captureException(e, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + handled: false, + }); + return event; }); - return event; - }); - return scope; - }); + return scope; + }); + } } - try { - return await next(); - } catch (e: unknown) { - if (typeof e === 'object' && e) { - if ('code' in e) { - // Is likely TRPCError - we only want to capture internal server errors - if (e.code === 'INTERNAL_SERVER_ERROR') { - captureError(e); - } - } else { - // Is likely random error that bubbles up - captureError(e); - } - } + let maybePromiseResult; + try { + maybePromiseResult = next(); + } catch (e) { + handleErrorCase(e); throw e; } + + if (isThenable(maybePromiseResult)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + Promise.resolve(maybePromiseResult).then(null, e => { + handleErrorCase(e); + }); + } + + // We return the original promise just to be safe. + return maybePromiseResult; }; } diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index fa6db4823643..f72262e64167 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -32,7 +32,8 @@ "@sentry/tracing": "file:../../../tracing", "@sentry-internal/tracing": "file:../../../tracing-internal", "@sentry/types": "file:../../../types", - "@sentry/utils": "file:../../../utils" + "@sentry/utils": "file:../../../utils", + "@vanilla-extract/css": "1.13.0" }, "engines": { "node": ">=14" diff --git a/packages/replay/src/coreHandlers/handleGlobalEvent.ts b/packages/replay/src/coreHandlers/handleGlobalEvent.ts index f69e8d975417..2607c28c4348 100644 --- a/packages/replay/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay/src/coreHandlers/handleGlobalEvent.ts @@ -2,9 +2,10 @@ import type { Event, EventHint } from '@sentry/types'; import { logger } from '@sentry/utils'; import type { ReplayContainer } from '../types'; -import { isErrorEvent, isReplayEvent, isTransactionEvent } from '../util/eventUtils'; +import { isErrorEvent, isFeedbackEvent, isReplayEvent, isTransactionEvent } from '../util/eventUtils'; import { isRrwebError } from '../util/isRrwebError'; import { handleAfterSendEvent } from './handleAfterSendEvent'; +import { addFeedbackBreadcrumb } from './util/addFeedbackBreadcrumb'; import { shouldSampleForBufferEvent } from './util/shouldSampleForBufferEvent'; /** @@ -30,8 +31,8 @@ export function handleGlobalEventListener( return event; } - // We only want to handle errors & transactions, nothing else - if (!isErrorEvent(event) && !isTransactionEvent(event)) { + // We only want to handle errors, transactions, and feedbacks, nothing else + if (!isErrorEvent(event) && !isTransactionEvent(event) && !isFeedbackEvent(event)) { return event; } @@ -41,6 +42,12 @@ export function handleGlobalEventListener( return event; } + if (isFeedbackEvent(event)) { + // Add a replay breadcrumb for this piece of feedback + addFeedbackBreadcrumb(replay, event); + return event; + } + // Unless `captureExceptions` is enabled, we want to ignore errors coming from rrweb // As there can be a bunch of stuff going wrong in internals there, that we don't want to bubble up to users if (isRrwebError(event, hint) && !replay.getOptions()._experiments.captureExceptions) { diff --git a/packages/replay/src/coreHandlers/util/addFeedbackBreadcrumb.ts b/packages/replay/src/coreHandlers/util/addFeedbackBreadcrumb.ts new file mode 100644 index 000000000000..5f2808d9b57d --- /dev/null +++ b/packages/replay/src/coreHandlers/util/addFeedbackBreadcrumb.ts @@ -0,0 +1,35 @@ +import { EventType } from '@sentry-internal/rrweb'; +import type { FeedbackEvent } from '@sentry/types'; + +import type { ReplayContainer } from '../../types'; + +/** + * Add a feedback breadcrumb event to replay. + */ +export function addFeedbackBreadcrumb(replay: ReplayContainer, event: FeedbackEvent): void { + replay.triggerUserActivity(); + replay.addUpdate(() => { + if (!event.timestamp) { + // Ignore events that don't have timestamps (this shouldn't happen, more of a typing issue) + // Return true here so that we don't flush + return true; + } + + void replay.throttledAddEvent({ + type: EventType.Custom, + timestamp: event.timestamp * 1000, + data: { + timestamp: event.timestamp, + tag: 'breadcrumb', + payload: { + category: 'sentry.feedback', + data: { + feedbackId: event.event_id, + }, + }, + }, + }); + + return false; + }); +} diff --git a/packages/replay/src/coreHandlers/util/fetchUtils.ts b/packages/replay/src/coreHandlers/util/fetchUtils.ts index 491fe727b1b8..80c6b4686a70 100644 --- a/packages/replay/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay/src/coreHandlers/util/fetchUtils.ts @@ -26,7 +26,7 @@ import { */ export async function captureFetchBreadcrumbToReplay( breadcrumb: Breadcrumb & { data: FetchBreadcrumbData }, - hint: FetchHint, + hint: Partial, options: ReplayNetworkOptions & { textEncoder: TextEncoderInternal; replay: ReplayContainer; @@ -50,12 +50,12 @@ export async function captureFetchBreadcrumbToReplay( */ export function enrichFetchBreadcrumb( breadcrumb: Breadcrumb & { data: FetchBreadcrumbData }, - hint: FetchHint, + hint: Partial, options: { textEncoder: TextEncoderInternal }, ): void { const { input, response } = hint; - const body = _getFetchRequestArgBody(input); + const body = input ? _getFetchRequestArgBody(input) : undefined; const reqSize = getBodySize(body, options.textEncoder); const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined; @@ -70,12 +70,13 @@ export function enrichFetchBreadcrumb( async function _prepareFetchData( breadcrumb: Breadcrumb & { data: FetchBreadcrumbData }, - hint: FetchHint, + hint: Partial, options: ReplayNetworkOptions & { textEncoder: TextEncoderInternal; }, ): Promise { - const { startTimestamp, endTimestamp } = hint; + const now = Date.now(); + const { startTimestamp = now, endTimestamp = now } = hint; const { url, @@ -106,10 +107,10 @@ async function _prepareFetchData( function _getRequestInfo( { networkCaptureBodies, networkRequestHeaders }: ReplayNetworkOptions, - input: FetchHint['input'], + input: FetchHint['input'] | undefined, requestBodySize?: number, ): ReplayNetworkRequestOrResponse | undefined { - const headers = getRequestHeaders(input, networkRequestHeaders); + const headers = input ? getRequestHeaders(input, networkRequestHeaders) : {}; if (!networkCaptureBodies) { return buildNetworkRequestOrResponse(headers, requestBodySize, undefined); @@ -130,16 +131,16 @@ async function _getResponseInfo( }: ReplayNetworkOptions & { textEncoder: TextEncoderInternal; }, - response: Response, + response: Response | undefined, responseBodySize?: number, ): Promise { if (!captureDetails && responseBodySize !== undefined) { return buildSkippedNetworkRequestOrResponse(responseBodySize); } - const headers = getAllHeaders(response.headers, networkResponseHeaders); + const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {}; - if (!networkCaptureBodies && responseBodySize !== undefined) { + if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) { return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); } @@ -163,7 +164,8 @@ async function _getResponseInfo( } return buildNetworkRequestOrResponse(headers, size, undefined); - } catch { + } catch (error) { + __DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize response body', error); // fallback return buildNetworkRequestOrResponse(headers, responseBodySize, undefined); } diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index 78b4e22efa9b..f75ecf0f9bc9 100644 --- a/packages/replay/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay/src/coreHandlers/util/networkUtils.ts @@ -1,5 +1,5 @@ import type { TextEncoderInternal } from '@sentry/types'; -import { dropUndefinedKeys, stringMatchesSomePattern } from '@sentry/utils'; +import { dropUndefinedKeys, logger, stringMatchesSomePattern } from '@sentry/utils'; import { NETWORK_BODY_MAX_SIZE, WINDOW } from '../../constants'; import type { @@ -62,16 +62,20 @@ export function parseContentLengthHeader(header: string | null | undefined): num /** Get the string representation of a body. */ export function getBodyString(body: unknown): string | undefined { - if (typeof body === 'string') { - return body; - } + try { + if (typeof body === 'string') { + return body; + } - if (body instanceof URLSearchParams) { - return body.toString(); - } + if (body instanceof URLSearchParams) { + return body.toString(); + } - if (body instanceof FormData) { - return _serializeFormData(body); + if (body instanceof FormData) { + return _serializeFormData(body); + } + } catch { + __DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize body', body); } return undefined; @@ -199,7 +203,6 @@ function normalizeNetworkBody(body: string | undefined): { } const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE; - const isProbablyJson = _strIsProbablyJson(body); if (exceedsSizeLimit) { diff --git a/packages/replay/src/coreHandlers/util/xhrUtils.ts b/packages/replay/src/coreHandlers/util/xhrUtils.ts index 8c02a8da6abf..b11f8575e2ad 100644 --- a/packages/replay/src/coreHandlers/util/xhrUtils.ts +++ b/packages/replay/src/coreHandlers/util/xhrUtils.ts @@ -20,7 +20,7 @@ import { */ export async function captureXhrBreadcrumbToReplay( breadcrumb: Breadcrumb & { data: XhrBreadcrumbData }, - hint: XhrHint, + hint: Partial, options: ReplayNetworkOptions & { replay: ReplayContainer }, ): Promise { try { @@ -41,11 +41,15 @@ export async function captureXhrBreadcrumbToReplay( */ export function enrichXhrBreadcrumb( breadcrumb: Breadcrumb & { data: XhrBreadcrumbData }, - hint: XhrHint, + hint: Partial, options: { textEncoder: TextEncoderInternal }, ): void { const { xhr, input } = hint; + if (!xhr) { + return; + } + const reqSize = getBodySize(input, options.textEncoder); const resSize = xhr.getResponseHeader('content-length') ? parseContentLengthHeader(xhr.getResponseHeader('content-length')) @@ -61,10 +65,11 @@ export function enrichXhrBreadcrumb( function _prepareXhrData( breadcrumb: Breadcrumb & { data: XhrBreadcrumbData }, - hint: XhrHint, + hint: Partial, options: ReplayNetworkOptions, ): ReplayNetworkRequestData | null { - const { startTimestamp, endTimestamp, input, xhr } = hint; + const now = Date.now(); + const { startTimestamp = now, endTimestamp = now, input, xhr } = hint; const { url, @@ -78,7 +83,7 @@ function _prepareXhrData( return null; } - if (!urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) { + if (!xhr || !urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) { const request = buildSkippedNetworkRequestOrResponse(requestBodySize); const response = buildSkippedNetworkRequestOrResponse(responseBodySize); return { @@ -98,16 +103,11 @@ function _prepareXhrData( : {}; const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders); - const request = buildNetworkRequestOrResponse( - networkRequestHeaders, - requestBodySize, - options.networkCaptureBodies ? getBodyString(input) : undefined, - ); - const response = buildNetworkRequestOrResponse( - networkResponseHeaders, - responseBodySize, - options.networkCaptureBodies ? hint.xhr.responseText : undefined, - ); + const requestBody = options.networkCaptureBodies ? getBodyString(input) : undefined; + const responseBody = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : undefined; + + const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody); + const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody); return { startTimestamp, @@ -133,3 +133,17 @@ function getResponseHeaders(xhr: XMLHttpRequest): Record { return acc; }, {}); } + +function _getXhrResponseBody(xhr: XMLHttpRequest): string | undefined { + try { + return xhr.responseText; + } catch {} // eslint-disable-line no-empty + + // Try to manually parse the response body, if responseText fails + try { + const response = xhr.response; + return getBodyString(response); + } catch {} // eslint-disable-line no-empty + + return undefined; +} diff --git a/packages/replay/src/types/replayFrame.ts b/packages/replay/src/types/replayFrame.ts index c65544ef8631..8c41173e1a8e 100644 --- a/packages/replay/src/types/replayFrame.ts +++ b/packages/replay/src/types/replayFrame.ts @@ -126,6 +126,15 @@ interface ReplayOptionFrame { useCompressionOption: boolean; } +interface ReplayFeedbackFrameData { + feedback_id: string; +} + +interface ReplayFeedbackFrame extends ReplayBaseBreadcrumbFrame { + category: 'sentry.feedback'; + data: ReplayFeedbackFrameData; +} + export type ReplayBreadcrumbFrame = | ReplayConsoleFrame | ReplayClickFrame @@ -136,6 +145,7 @@ export type ReplayBreadcrumbFrame = | ReplaySlowClickFrame | ReplayMultiClickFrame | ReplayMutationFrame + | ReplayFeedbackFrame | ReplayBaseBreadcrumbFrame; interface ReplayBaseSpanFrame { diff --git a/packages/replay/src/util/eventUtils.ts b/packages/replay/src/util/eventUtils.ts index 5cd4989a8267..8d2256bb57e3 100644 --- a/packages/replay/src/util/eventUtils.ts +++ b/packages/replay/src/util/eventUtils.ts @@ -1,4 +1,4 @@ -import type { ErrorEvent, Event, ReplayEvent, TransactionEvent } from '@sentry/types'; +import type { ErrorEvent, Event, FeedbackEvent, ReplayEvent, TransactionEvent } from '@sentry/types'; /** If the event is an error event */ export function isErrorEvent(event: Event): event is ErrorEvent { @@ -14,3 +14,8 @@ export function isTransactionEvent(event: Event): event is TransactionEvent { export function isReplayEvent(event: Event): event is ReplayEvent { return event.type === 'replay_event'; } + +/** If the event is a feedback event */ +export function isFeedbackEvent(event: Event): event is FeedbackEvent { + return event.type === 'feedback'; +} diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index bf98be00e163..df43ec441391 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -101,7 +101,7 @@ export class Prisma implements Integration { return trace( { name: model ? `${model} ${action}` : action, - op: 'db.sql.prisma', + op: 'db.prisma', origin: 'auto.db.prisma', data: { ...clientData, 'db.operation': action }, }, diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts index 4b2034f37a01..4debafd7de35 100644 --- a/packages/tracing/test/integrations/node/prisma.test.ts +++ b/packages/tracing/test/integrations/node/prisma.test.ts @@ -55,7 +55,7 @@ describe('setupOnce', function () { expect(mockTrace).toHaveBeenLastCalledWith( { name: 'user create', - op: 'db.sql.prisma', + op: 'db.prisma', origin: 'auto.db.prisma', data: { 'db.system': 'postgresql', 'db.prisma.version': '3.1.2', 'db.operation': 'create' }, },