From 9b0abcdb4d66ccd6da3125bc12d16560bbfa9299 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 25 Oct 2023 13:56:19 +0200 Subject: [PATCH 01/17] fix(nextjs): Instrument route handlers with `jsx` and `tsx` file extensions (#9362) --- packages/nextjs/src/config/webpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 278f4dd71ed5..f4dd1d7ecfae 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -182,7 +182,7 @@ export function constructWebpackConfigFunction( const normalizedAbsoluteResourcePath = normalizeLoaderResourcePath(resourcePath); return ( normalizedAbsoluteResourcePath.startsWith(appDirPath + path.sep) && - !!normalizedAbsoluteResourcePath.match(/[\\/]route\.(js|ts)$/) + !!normalizedAbsoluteResourcePath.match(/[\\/]route\.(js|jsx|ts|tsx)$/) ); }; From f0b02819826e0b50ac57dcd41e3e662fea5d42de Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 25 Oct 2023 14:15:59 +0200 Subject: [PATCH 02/17] fix(deno): Build Sentry dependencies for tests (#9312) Co-authored-by: Luca Forstner --- .gitignore | 2 - packages/deno/.gitignore | 2 + packages/deno/package.json | 4 +- packages/deno/rollup.test.config.js | 42 +++++++++++++++++++ .../deno/test/__snapshots__/mod.test.ts.snap | 8 ++-- packages/deno/test/build.ts | 4 ++ packages/deno/test/mod.test.ts | 16 ++++--- packages/deno/test/normalize.ts | 27 ++++++------ packages/deno/test/transport.ts | 27 +++++------- packages/deno/tsconfig.test.types.json | 10 +++++ 10 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 packages/deno/rollup.test.config.js create mode 100644 packages/deno/test/build.ts create mode 100644 packages/deno/tsconfig.test.types.json diff --git a/.gitignore b/.gitignore index d6eee47e4eed..777b23658572 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,6 @@ jest/transformers/*.js # node tarballs packages/*/sentry-*.tgz .nxcache -# The Deno types are downloaded before building -packages/deno/lib.deno.d.ts # logs yarn-error.log diff --git a/packages/deno/.gitignore b/packages/deno/.gitignore index 299ae4a5c2fd..d2de144a353c 100644 --- a/packages/deno/.gitignore +++ b/packages/deno/.gitignore @@ -1 +1,3 @@ build-types +build-test +lib.deno.d.ts diff --git a/packages/deno/package.json b/packages/deno/package.json index 54c9d174f018..24878690baad 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -48,7 +48,9 @@ "lint:eslint": "eslint . --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", "install:deno": "node ./scripts/install-deno.mjs", - "test": "run-s deno-types install:deno test:types test:unit", + "pretest": "run-s deno-types test:build", + "test": "run-s install:deno test:types test:unit", + "test:build": "tsc -p tsconfig.test.types.json && rollup -c rollup.test.config.js", "test:types": "deno check ./build/index.js", "test:unit": "deno test --allow-read --allow-run", "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update", diff --git a/packages/deno/rollup.test.config.js b/packages/deno/rollup.test.config.js new file mode 100644 index 000000000000..3947d0e94b16 --- /dev/null +++ b/packages/deno/rollup.test.config.js @@ -0,0 +1,42 @@ +// @ts-check +import dts from 'rollup-plugin-dts'; +import nodeResolve from '@rollup/plugin-node-resolve'; +import sucrase from '@rollup/plugin-sucrase'; +import { defineConfig } from 'rollup'; + +export default [ + defineConfig({ + input: ['test/build.ts'], + output: { + file: 'build-test/index.js', + sourcemap: true, + preserveModules: false, + strict: false, + freeze: false, + interop: 'auto', + format: 'esm', + banner: '/// ', + }, + plugins: [ + nodeResolve({ + extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx'], + }), + sucrase({ transforms: ['typescript'] }), + ], + }), + defineConfig({ + input: './build-test/build.d.ts', + output: [{ file: 'build-test/index.d.ts', format: 'es' }], + plugins: [ + dts({ respectExternal: true }), + // The bundled types contain a declaration for the __DEBUG_BUILD__ global + // This can result in errors about duplicate global declarations so we strip it out! + { + name: 'strip-global', + renderChunk(code) { + return { code: code.replace(/declare global \{\s*const __DEBUG_BUILD__: boolean;\s*\}/g, '') }; + }, + }, + ], + }), +]; diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 2cdb246a8e8b..501508e1a268 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -82,7 +82,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "", in_app: true, - lineno: 42, + lineno: 46, post_context: [ "", " await delay(200);", @@ -90,7 +90,7 @@ snapshot[`captureException 1`] = ` "});", "", "Deno.test('captureMessage', async t => {", - " let ev: Event | undefined;", + " let ev: sentryTypes.Event | undefined;", ], pre_context: [ " ev = event;", @@ -108,7 +108,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "something", in_app: true, - lineno: 39, + lineno: 43, post_context: [ " }", "", @@ -120,7 +120,7 @@ snapshot[`captureException 1`] = ` ], pre_context: [ "Deno.test('captureException', async t => {", - " let ev: Event | undefined;", + " let ev: sentryTypes.Event | undefined;", " const [hub] = getTestClient(event => {", " ev = event;", " });", diff --git a/packages/deno/test/build.ts b/packages/deno/test/build.ts new file mode 100644 index 000000000000..b593fed2f4dd --- /dev/null +++ b/packages/deno/test/build.ts @@ -0,0 +1,4 @@ +// We use this as the entry point to bundle Sentry dependencies that are used by the tests. +export * as sentryTypes from '@sentry/types'; +export * as sentryUtils from '@sentry/utils'; +export * as sentryCore from '@sentry/core'; diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index 7f57616816d5..f457033efe96 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -1,20 +1,24 @@ import { assertEquals } from 'https://deno.land/std@0.202.0/assert/assert_equals.ts'; import { assertSnapshot } from 'https://deno.land/std@0.202.0/testing/snapshot.ts'; -import { createStackParser, nodeStackLineParser } from '../../utils/build/esm/index.js'; +import type { sentryTypes } from '../build-test/index.js'; +import { sentryUtils } from '../build-test/index.js'; import { defaultIntegrations, DenoClient, Hub, Scope } from '../build/index.js'; import { getNormalizedEvent } from './normalize.ts'; import { makeTestTransport } from './transport.ts'; -function getTestClient(callback: (event?: Event) => void, integrations: any[] = []): [Hub, DenoClient] { +function getTestClient( + callback: (event?: sentryTypes.Event) => void, + integrations: sentryTypes.Integration[] = [], +): [Hub, DenoClient] { const client = new DenoClient({ dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', debug: true, integrations: [...defaultIntegrations, ...integrations], - stackParser: createStackParser(nodeStackLineParser()), + stackParser: sentryUtils.createStackParser(sentryUtils.nodeStackLineParser()), transport: makeTestTransport(envelope => { callback(getNormalizedEvent(envelope)); - }) as any, + }), }); const scope = new Scope(); @@ -30,7 +34,7 @@ function delay(time: number): Promise { } Deno.test('captureException', async t => { - let ev: Event | undefined; + let ev: sentryTypes.Event | undefined; const [hub] = getTestClient(event => { ev = event; }); @@ -46,7 +50,7 @@ Deno.test('captureException', async t => { }); Deno.test('captureMessage', async t => { - let ev: Event | undefined; + let ev: sentryTypes.Event | undefined; const [hub] = getTestClient(event => { ev = event; }); diff --git a/packages/deno/test/normalize.ts b/packages/deno/test/normalize.ts index 45f631116955..64295932e00d 100644 --- a/packages/deno/test/normalize.ts +++ b/packages/deno/test/normalize.ts @@ -1,20 +1,21 @@ /* eslint-disable complexity */ -import { forEachEnvelopeItem } from '../../utils/build/esm/index.js'; +import type { sentryTypes } from '../build-test/index.js'; +import { sentryUtils } from '../build-test/index.js'; -type EventOrSession = any; +type EventOrSession = sentryTypes.Event | sentryTypes.Transaction | sentryTypes.Session; -export function getNormalizedEvent(envelope: any): any | undefined { - let event: any | undefined; +export function getNormalizedEvent(envelope: sentryTypes.Envelope): sentryTypes.Event | undefined { + let event: sentryTypes.Event | undefined; - forEachEnvelopeItem(envelope, (item: any) => { + sentryUtils.forEachEnvelopeItem(envelope, item => { const [headers, body] = item; if (headers.type === 'event') { - event = body; + event = body as sentryTypes.Event; } }); - return normalize(event) as any | undefined; + return normalize(event) as sentryTypes.Event | undefined; } export function normalize(event: EventOrSession | undefined): EventOrSession | undefined { @@ -23,14 +24,14 @@ export function normalize(event: EventOrSession | undefined): EventOrSession | u } if (eventIsSession(event)) { - return normalizeSession(event); + return normalizeSession(event as sentryTypes.Session); } else { - return normalizeEvent(event); + return normalizeEvent(event as sentryTypes.Event); } } export function eventIsSession(data: EventOrSession): boolean { - return !!data?.sid; + return !!(data as sentryTypes.Session)?.sid; } /** @@ -39,7 +40,7 @@ export function eventIsSession(data: EventOrSession): boolean { * All properties that are timestamps, versions, ids or variables that may vary * by platform are replaced with placeholder strings */ -function normalizeSession(session: any): any { +function normalizeSession(session: sentryTypes.Session): sentryTypes.Session { if (session.sid) { session.sid = '{{id}}'; } @@ -65,7 +66,7 @@ function normalizeSession(session: any): any { * All properties that are timestamps, versions, ids or variables that may vary * by platform are replaced with placeholder strings */ -function normalizeEvent(event: any): any { +function normalizeEvent(event: sentryTypes.Event): sentryTypes.Event { if (event.sdk?.version) { event.sdk.version = '{{version}}'; } @@ -153,7 +154,7 @@ function normalizeEvent(event: any): any { if (event.exception?.values?.[0].stacktrace?.frames) { // Exlcude Deno frames since these may change between versions event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames.filter( - (frame: any) => !frame.filename?.includes('deno:'), + frame => !frame.filename?.includes('deno:'), ); } diff --git a/packages/deno/test/transport.ts b/packages/deno/test/transport.ts index 2eaeed6eeef6..47cb86622cb7 100644 --- a/packages/deno/test/transport.ts +++ b/packages/deno/test/transport.ts @@ -1,30 +1,25 @@ -import { createTransport } from 'npm:@sentry/core'; -import type { - BaseTransportOptions, - Envelope, - Transport, - TransportMakeRequestResponse, - TransportRequest, -} from 'npm:@sentry/types'; -import { parseEnvelope } from 'npm:@sentry/utils'; +import type { sentryTypes } from '../build-test/index.js'; +import { sentryCore, sentryUtils } from '../build-test/index.js'; -export interface TestTransportOptions extends BaseTransportOptions { - callback: (envelope: Envelope) => void; +export interface TestTransportOptions extends sentryTypes.BaseTransportOptions { + callback: (envelope: sentryTypes.Envelope) => void; } /** * Creates a Transport that uses the Fetch API to send events to Sentry. */ -export function makeTestTransport(callback: (envelope: Envelope) => void) { - return (options: BaseTransportOptions): Transport => { - async function doCallback(request: TransportRequest): Promise { - await callback(parseEnvelope(request.body, new TextEncoder(), new TextDecoder())); +export function makeTestTransport(callback: (envelope: sentryTypes.Envelope) => void) { + return (options: sentryTypes.BaseTransportOptions): sentryTypes.Transport => { + async function doCallback( + request: sentryTypes.TransportRequest, + ): Promise { + await callback(sentryUtils.parseEnvelope(request.body, new TextEncoder(), new TextDecoder())); return Promise.resolve({ statusCode: 200, }); } - return createTransport(options, doCallback); + return sentryCore.createTransport(options, doCallback); }; } diff --git a/packages/deno/tsconfig.test.types.json b/packages/deno/tsconfig.test.types.json new file mode 100644 index 000000000000..1cac4cb38a90 --- /dev/null +++ b/packages/deno/tsconfig.test.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["./lib.deno.d.ts", "test/build.ts"], + "compilerOptions": { + "declaration": true, + "declarationMap": false, + "emitDeclarationOnly": true, + "outDir": "build-test" + } +} From 207b0bc352fba1d77cb9c872d19cb5388d3826ef Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 25 Oct 2023 15:14:02 +0200 Subject: [PATCH 03/17] fix(replay): ensure `replay_id` is not added to DSC if session expired (#9359) We noticed we still sometimes see "too long sessions", and it seems this is because of errors being tagged with a replay long after the session expired. After some digging, this may be because we do not check session expiration when adding the `replay_id` to the DSC. This PR fixes this. --- packages/replay/src/util/addGlobalListeners.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index 0948561f7852..1f7945565524 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -40,7 +40,11 @@ export function addGlobalListeners(replay: ReplayContainer): void { const replayId = replay.getSessionId(); // We do not want to set the DSC when in buffer mode, as that means the replay has not been sent (yet) if (replayId && replay.isEnabled() && replay.recordingMode === 'session') { - dsc.replay_id = replayId; + // Ensure to check that the session is still active - it could have expired in the meanwhile + const isSessionActive = replay.checkAndHandleExpiredSession(); + if (isSessionActive) { + dsc.replay_id = replayId; + } } }); From 39ba7d4c2b5dbc10d64e6328ce605c752ea19817 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 25 Oct 2023 11:12:41 -0400 Subject: [PATCH 04/17] fix(serverless): Don't mark all errors as unhandled (#9368) --- packages/serverless/src/awslambda.ts | 3 +-- packages/serverless/src/gcpfunction/index.ts | 2 -- packages/serverless/src/utils.ts | 16 ------------- packages/serverless/test/awslambda.test.ts | 25 -------------------- packages/serverless/test/gcpfunction.test.ts | 25 -------------------- 5 files changed, 1 insertion(+), 70 deletions(-) diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index e29f77aaf879..78578083ee72 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -14,7 +14,7 @@ import { performance } from 'perf_hooks'; import { types } from 'util'; import { AWSServices } from './awsservices'; -import { markEventUnhandled, serverlessEventProcessor } from './utils'; +import { markEventUnhandled } from './utils'; export * from '@sentry/node'; @@ -88,7 +88,6 @@ export function init(options: AWSLambdaOptions = {}): void { }; Sentry.init(opts); - Sentry.addGlobalEventProcessor(serverlessEventProcessor); } /** */ diff --git a/packages/serverless/src/gcpfunction/index.ts b/packages/serverless/src/gcpfunction/index.ts index aa8f800d0d52..dc4358b30368 100644 --- a/packages/serverless/src/gcpfunction/index.ts +++ b/packages/serverless/src/gcpfunction/index.ts @@ -3,7 +3,6 @@ import type { Integration, SdkMetadata } from '@sentry/types'; import { GoogleCloudGrpc } from '../google-cloud-grpc'; import { GoogleCloudHttp } from '../google-cloud-http'; -import { serverlessEventProcessor } from '../utils'; export * from './http'; export * from './events'; @@ -38,5 +37,4 @@ export function init(options: Sentry.NodeOptions = {}): void { }; Sentry.init(opts); - Sentry.addGlobalEventProcessor(serverlessEventProcessor); } diff --git a/packages/serverless/src/utils.ts b/packages/serverless/src/utils.ts index 69e28ab3a823..c7377c39e73e 100644 --- a/packages/serverless/src/utils.ts +++ b/packages/serverless/src/utils.ts @@ -1,23 +1,7 @@ import { runWithAsyncContext } from '@sentry/core'; -import type { Event } from '@sentry/node'; import type { Scope } from '@sentry/types'; import { addExceptionMechanism } from '@sentry/utils'; -/** - * Event processor that will override SDK details to point to the serverless SDK instead of Node, - * as well as set correct mechanism type, which should be set to `handled: false`. - * We do it like this so that we don't introduce any side-effects in this module, which makes it tree-shakeable. - * @param event Event - * @param integration Name of the serverless integration ('AWSLambda', 'GCPFunction', etc) - */ -export function serverlessEventProcessor(event: Event): Event { - addExceptionMechanism(event, { - handled: false, - }); - - return event; -} - /** * @param fn function to run * @returns function which runs in the newly created domain or in the existing one diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 199d0ac295ab..a3542d058121 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -534,30 +534,5 @@ describe('AWSLambda', () => { }), ); }); - - test('enhance event with correct mechanism value', () => { - const eventWithSomeData = { - exception: { - values: [{}], - }, - }; - - // @ts-expect-error see "Why @ts-expect-error" note - Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); - Sentry.AWSLambda.init({}); - - expect(eventWithSomeData).toEqual({ - exception: { - values: [ - { - mechanism: { - handled: false, - type: 'generic', - }, - }, - ], - }, - }); - }); }); }); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 5f830b05cdee..6aef838b1eb3 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -684,30 +684,5 @@ describe('GCPFunction', () => { }), ); }); - - test('enhance event with correct mechanism value', () => { - const eventWithSomeData = { - exception: { - values: [{}], - }, - }; - - // @ts-expect-error see "Why @ts-expect-error" note - Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); - Sentry.GCPFunction.init({}); - - expect(eventWithSomeData).toEqual({ - exception: { - values: [ - { - mechanism: { - handled: false, - type: 'generic', - }, - }, - ], - }, - }); - }); }); }); From 25542d38d869bc835cd52b8caac701af816ef8fa Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 25 Oct 2023 18:21:05 +0200 Subject: [PATCH 05/17] feat(nextjs): Instrument SSR page components (#9346) --- .../pages/pages-router/ssr-error-class.tsx | 16 +++++ .../pages/pages-router/ssr-error-fc.tsx | 12 ++++ .../tests/pages-ssr-errors.test.ts | 24 +++++++ packages/nextjs/src/common/index.ts | 2 + .../src/common/wrapPageComponentWithSentry.ts | 66 +++++++++++++++++++ .../config/templates/pageWrapperTemplate.ts | 2 +- packages/nextjs/src/index.types.ts | 5 ++ 7 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts create mode 100644 packages/nextjs/src/common/wrapPageComponentWithSentry.ts diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx new file mode 100644 index 000000000000..86ce68c1c034 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export default class Page extends React.Component { + render() { + throw new Error('Pages SSR Error Class'); + return
Hello world!
; + } +} + +export function getServerSideProps() { + return { + props: { + foo: 'bar', + }, + }; +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx new file mode 100644 index 000000000000..6342caec47ca --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx @@ -0,0 +1,12 @@ +export default function Page() { + throw new Error('Pages SSR Error FC'); + return
Hello world!
; +} + +export function getServerSideProps() { + return { + props: { + foo: 'bar', + }, + }; +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts new file mode 100644 index 000000000000..2c1eb9729fdd --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; + +test('Will capture error for SSR rendering error (Class Component)', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class'; + }); + + await page.goto('/pages-router/ssr-error-class'); + + const errorEvent = await errorEventPromise; + expect(errorEvent).toBeDefined(); +}); + +test('Will capture error for SSR rendering error (Functional Component)', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC'; + }); + + await page.goto('/pages-router/ssr-error-fc'); + + const errorEvent = await errorEventPromise; + expect(errorEvent).toBeDefined(); +}); diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index 3aeef33af760..2f166b3e4b59 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -41,3 +41,5 @@ export { wrapRouteHandlerWithSentry } from './wrapRouteHandlerWithSentry'; export { wrapApiHandlerWithSentryVercelCrons } from './wrapApiHandlerWithSentryVercelCrons'; export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry'; + +export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry'; diff --git a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts new file mode 100644 index 000000000000..d67dd2a544c0 --- /dev/null +++ b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts @@ -0,0 +1,66 @@ +import { captureException } from '@sentry/core'; +import { addExceptionMechanism } from '@sentry/utils'; + +interface FunctionComponent { + (...args: unknown[]): unknown; +} + +interface ClassComponent { + new (...args: unknown[]): { + render(...args: unknown[]): unknown; + }; +} + +function isReactClassComponent(target: unknown): target is ClassComponent { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return typeof target === 'function' && target?.prototype?.isReactComponent; +} + +/** + * Wraps a page component with Sentry error instrumentation. + */ +export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | ClassComponent): unknown { + if (isReactClassComponent(pageComponent)) { + return class SentryWrappedPageComponent extends pageComponent { + public render(...args: unknown[]): unknown { + try { + return super.render(...args); + } catch (e) { + captureException(e, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + handled: false, + }); + return event; + }); + + return scope; + }); + throw e; + } + } + }; + } else if (typeof pageComponent === 'function') { + return new Proxy(pageComponent, { + apply(target, thisArg, argArray) { + try { + return target.apply(thisArg, argArray); + } catch (e) { + captureException(e, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + handled: false, + }); + return event; + }); + + return scope; + }); + throw e; + } + }, + }); + } else { + return pageComponent; + } +} diff --git a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts index 16cce1a6cc39..c383503f42cf 100644 --- a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -49,7 +49,7 @@ export const getServerSideProps = ? Sentry.wrapGetServerSidePropsWithSentry(origGetServerSideProps, '__ROUTE__') : undefined; -export default pageComponent; +export default pageComponent ? Sentry.wrapPageComponentWithSentry(pageComponent as unknown) : pageComponent; // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to // not include anything whose name matchs something we've explicitly exported above. diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 479760633b54..804c8f2b3e35 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -186,3 +186,8 @@ export declare function wrapApiHandlerWithSentryVercelCrons(WrappingTarget: C): C; From 677cb02a56511825c4440cdbdbf6fe24be73c267 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 25 Oct 2023 18:33:34 +0200 Subject: [PATCH 06/17] ci(e2e): Restore nxcache in e2e tests (#9370) --- .github/workflows/build.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66b282202f5e..806bfd4edc2f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -817,13 +817,20 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Check tarball cache - uses: actions/cache@v3 + - name: NX cache + uses: actions/cache/restore@v3 with: - path: ${{ github.workspace }}/packages/*/*.tgz - key: ${{ env.BUILD_CACHE_TARBALL_KEY }} + path: .nxcache + key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT }} + # On develop branch, we want to _store_ the cache (so it can be used by other branches), but never _restore_ from it + restore-keys: ${{ env.NX_CACHE_RESTORE_KEYS }} - name: Build tarballs run: yarn build:tarball + - name: Stores tarballs in cache + uses: actions/cache/save@v3 + with: + path: ${{ github.workspace }}/packages/*/*.tgz + key: ${{ env.BUILD_CACHE_TARBALL_KEY }} job_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test From d468f5880bd98864d8fb5eaea3e0d7fee258cb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=BDubom=C3=ADrIgonda?= <9303146+LubomirIgonda1@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:46:13 +0200 Subject: [PATCH 07/17] fix(tracing-internal): Fix case when middleware contain array of routes with special chars as @ (#9375) --- .../src/node/integrations/express.ts | 54 ++++++++++--------- .../test/node/express.test.ts | 21 ++++++++ 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts index e46096d9ed84..f7ff20fa9986 100644 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ b/packages/tracing-internal/src/node/integrations/express.ts @@ -65,7 +65,7 @@ type Layer = { route?: { path: RouteType | RouteType[] }; path?: string; regexp?: RegExp; - keys?: { name: string; offset: number; optional: boolean }[]; + keys?: { name: string | number; offset: number; optional: boolean }[]; }; type RouteType = string | RegExp; @@ -433,30 +433,34 @@ export const extractOriginalRoute = ( /** * iterate param matches from regexp.exec */ - paramIndices.forEach(([startOffset, endOffset], index: number) => { - /** - * isolate part before param - */ - const substr1 = resultPath.substring(0, startOffset - indexShift); - /** - * define paramName as replacement in format :pathParam - */ - const replacement = `:${orderedKeys[index].name}`; - - /** - * isolate part after param - */ - const substr2 = resultPath.substring(endOffset - indexShift); - - /** - * recreate original path but with param replacement - */ - resultPath = substr1 + replacement + substr2; - - /** - * calculate new index shift after resultPath was modified - */ - indexShift = indexShift + (endOffset - startOffset - replacement.length); + paramIndices.forEach((item: [number, number] | undefined, index: number) => { + /** check if offsets is define because in some cases regex d flag returns undefined */ + if (item) { + const [startOffset, endOffset] = item; + /** + * isolate part before param + */ + const substr1 = resultPath.substring(0, startOffset - indexShift); + /** + * define paramName as replacement in format :pathParam + */ + const replacement = `:${orderedKeys[index].name}`; + + /** + * isolate part after param + */ + const substr2 = resultPath.substring(endOffset - indexShift); + + /** + * recreate original path but with param replacement + */ + resultPath = substr1 + replacement + substr2; + + /** + * calculate new index shift after resultPath was modified + */ + indexShift = indexShift + (endOffset - startOffset - replacement.length); + } }); return resultPath; diff --git a/packages/tracing-internal/test/node/express.test.ts b/packages/tracing-internal/test/node/express.test.ts index e9f4df236b33..4b8d31fb2cdc 100644 --- a/packages/tracing-internal/test/node/express.test.ts +++ b/packages/tracing-internal/test/node/express.test.ts @@ -87,5 +87,26 @@ if (major >= 16) { ]; expect(extractOriginalRoute(path, regex, keys)).toBe('/user/:userId/profile/:username'); }); + + it('should handle complex regex scheme extract from array of routes', () => { + const path1 = '/@fs/*'; + const path2 = '/@vite/client'; + const path3 = '/@react-refresh'; + const path4 = '/manifest.json'; + + const regex = + /(?:^\/manifest\.json\/?(?=\/|$)|^\/@vite\/client\/?(?=\/|$)|^\/@react-refresh\/?(?=\/|$)|^\/src\/(.*)\/?(?=\/|$)|^\/vite\/(.*)\/?(?=\/|$)|^\/node_modules\/(.*)\/?(?=\/|$)|^\/@fs\/(.*)\/?(?=\/|$)|^\/@vite-plugin-checker-runtime\/?(?=\/|$)|^\/?$\/?(?=\/|$)|^\/home\/?$\/?(?=\/|$)|^\/login\/?(?=\/|$))/; + const keys = [ + { name: 0, offset: 8, optional: false }, + { name: 0, offset: 8, optional: false }, + { name: 0, offset: 9, optional: false }, + { name: 0, offset: 17, optional: false }, + ]; + + expect(extractOriginalRoute(path1, regex, keys)).toBe('/@fs/:0'); + expect(extractOriginalRoute(path2, regex, keys)).toBe('/@vite/client'); + expect(extractOriginalRoute(path3, regex, keys)).toBe('/@react-refresh'); + expect(extractOriginalRoute(path4, regex, keys)).toBe('/manifest.json'); + }); }); } From 8b7b81e0daedcc269ae0de931ba549ec4cfe710c Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 26 Oct 2023 13:44:59 -0400 Subject: [PATCH 08/17] feat(feedback): Add Feedback SDK instructions to README (#9351) --- packages/feedback/README.md | 185 ++++++++++++++++++++++++++++-------- 1 file changed, 145 insertions(+), 40 deletions(-) diff --git a/packages/feedback/README.md b/packages/feedback/README.md index 799cea6b59b8..9ae144057605 100644 --- a/packages/feedback/README.md +++ b/packages/feedback/README.md @@ -11,28 +11,33 @@ This SDK is **considered experimental and in an alpha state**. It may experience ## Pre-requisites -`@sentry/feedback` currently can only be used by browsers with [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) support. +`@sentry-internal/feedback` currently can only be used by browsers with [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) support. ## Installation -Feedback can be imported from `@sentry/browser`, or a respective SDK package like `@sentry/react` or `@sentry/vue`. -You don't need to install anything in order to use Feedback. The minimum version that includes Feedback is <>. +During the alpha phase, the feedback integration will need to be imported from `@sentry-internal/feedback`. This will be +changed for the general release. -For details on using Feedback when using Sentry via the CDN bundles, see [CDN bundle](#loading-feedback-as-a-cdn-bundle). +```shell +npm add @sentry-internal/feedback +``` ## Setup -To set up the integration, add the following to your Sentry initialization. Several options are supported and passable via the integration constructor. -See the [configuration section](#configuration) below for more details. +To set up the integration, add the following to your Sentry initialization. This will inject a feedback button to the bottom right corner of your application. Users can then click it to open up a feedback form where they can submit feedback. + +Several options are supported and passable via the integration constructor. See the [configuration section](#configuration) below for more details. ```javascript import * as Sentry from '@sentry/browser'; -// or e.g. import * as Sentry from '@sentry/react'; +// or from a framework specific SDK, e.g. +// import * as Sentry from '@sentry/react'; +import Feedback from '@sentry-internal/feedback'; Sentry.init({ dsn: '__DSN__', integrations: [ - new Sentry.Feedback({ + new Feedback({ // Additional SDK configuration goes in here, for example: // See below for all available options }) @@ -41,59 +46,159 @@ Sentry.init({ }); ``` -### Lazy loading Feedback +## Configuration + +### General Integration Configuration -Feedback will start automatically when you add the integration. -If you do not want to start Feedback immediately (e.g. if you want to lazy-load it), -you can also use `addIntegration` to load it later: +The following options can be configured as options to the integration, in `new Feedback({})`: -```js -import * as Sentry from "@sentry/react"; -import { BrowserClient } from "@sentry/browser"; +| key | type | default | description | +| --------- | ------- | ------- | ----------- | +| `autoInject` | `boolean` | `true` | Injects the Feedback widget into the application when the integration is added. This is useful to turn off if you bring your own button, or only want to show the widget on certain views. | +| `colorScheme` | `"system" \| "light" \| "dark"` | `"system"` | The color theme to use. `"system"` will follow your OS colorscheme. | -Sentry.init({ - // Do not load it initially - integrations: [] +### User/form Related Configuration +| key | type | default | description | +| --------- | ------- | ------- | ----------- | +| `showName` | `boolean` | `true` | Displays the name field on the feedback form, however will still capture the name (if available) from Sentry SDK context. | +| `showEmail` | `boolean` | `true` | Displays the email field on the feedback form, however will still capture the email (if available) from Sentry SDK context. | +| `isAnonymous` | `boolean` | `false` | Hides both name and email fields and does not use Sentry SDK's user context. | +| `useSentryUser` | `Record` | `{ email: 'email', name: 'username'}` | Map of the `email` and `name` fields to the corresponding Sentry SDK user fields that were called with `Sentry.setUser`. | + +By default the Feedback integration will attempt to fill in the name/email fields if you have set a user context via [`Sentry.setUser`](https://docs.sentry.io/platforms/javascript/enriching-events/identify-user/). By default it expects the email and name fields to be `email` and `username`. Below is an example configuration with non-default user fields. + +```javascript +Sentry.setUser({ + email: 'foo@example.com', + fullName: 'Jane Doe', }); -// Sometime later -const { Feedback } = await import('@sentry/browser'); -const client = Sentry.getCurrentHub().getClient(); -// Client can be undefined -client?.addIntegration(new Feedback()); +new Feedback({ + useSentryUser({ + email: 'email', + name: 'fullName', + }), +}) +``` + +### Text Customization +Most text that you see in the default Feedback widget can be customized. + +| key | default | description | +| --------- | ------- | ----------- | +| `buttonLabel` | `"Feedback"` | The label of the widget button. | +| `submitButtonLabel` | `"Send Feedback"` | The label of the submit button used in the feedback form dialog. | +| `cancelButtonLabel` | `"Cancel"` | The label of the cancel button used in the feedback form dialog. | +| `formTitle` | `"Send Feedback"` | The title at the top of the feedback form dialog. | +| `nameLabel` | `"Full Name"` | The label of the name input field. | +| `namePlaceholder` | `"Full Name"` | The placeholder for the name input field. | +| `emailLabel` | `"Email"` | The label of the email input field. || +| `emailPlaceholder` | `"Email"` | The placeholder for the email input field. | +| `messageLabel` | `"Description"` | The label for the feedback description input field. | +| `messagePlaceholder` | `"What's the issue? What did you expect?"` | The placeholder for the feedback description input field. | +| `successMessageText` | `"Thank you for your report!"` | The message to be displayed after a succesful feedback submission. | + +```javascript +new Feedback({ + buttonLabel: 'Bug Report', + submitButtonLabel: 'Send Report', + formTitle: 'Send Bug Report', +}); ``` -### Identifying Users +### Theme Customization +Colors can be customized via the Feedback constructor or by defining CSS variables on the widget button. If you use the default widget button, it will have an `id="sentry-feedback`, meaning you can use the `#sentry-feedback` selector to define CSS variables to override. -If you have only followed the above instructions to setup session feedbacks, you will only see IP addresses in Sentry's UI. In order to associate a user identity to a session feedback, use [`setUser`](https://docs.sentry.io/platforms/javascript/enriching-events/identify-user/). +| key | css variable | light | dark | description | +| --- | --- | --- | --- | --- | +| `background` | `--bg-color` | `#ffffff` | `#29232f` | Background color of the widget actor and dialog. | +| `backgroundHover` | `--bg-hover-color` | `#f6f6f7` | `#352f3b` | The background color of widget actor when in a hover state | +| `foreground` | `--fg-color` | `#2b2233` | `#ebe6ef` | The foreground color, e.g. text color | +| `error` | `--error-color` | `#df3338` | `#f55459` | Color used for error related components (e.g. text color when there was an error submitting feedback) | +| `success` | `--success-color` | `#268d75` | `#2da98c` | Color used for success-related components (e.g. text color when feedback is submitted successfully) | +| `border` | `--border` | `1.5px solid rgba(41, 35, 47, 0.13)` | `1.5px solid rgba(235, 230, 239, 0.15)` | The border style used for the widget actor and dialog | +| `boxShadow` | `--box-shadow` | `0px 4px 24px 0px rgba(43, 34, 51, 0.12)` | `0px 4px 24px 0px rgba(43, 34, 51, 0.12)` | The box shadow style used for the widget actor and dialog | +Here is an example of customizing only the background color for the light theme using the Feedback constructor configuration. ```javascript -import * as Sentry from "@sentry/browser"; +new Feedback({ + themeLight: { + background: "#cccccc", + }, +}) +``` + +Or the same example above but using the CSS variables method: -Sentry.setUser({ email: "jane.doe@example.com" }); +```css +#sentry-feedback { + --bg-color: #cccccc; +} ``` -## Loading Feedback as a CDN Bundle +### Additional UI Customization +Similar to theme customization above, these are additional CSS variables that can be overridden. Note these are not supported in the constructor. -As an alternative to the NPM package, you can use Feedback as a CDN bundle. -Please refer to the [Feedback installation guide](https://docs.sentry.io/platforms/javascript/session-feedback/#install) for CDN bundle instructions. +| Variable | Default | Description | +| --- | --- | --- | +| `--bottom` | `1rem` | By default the widget has a position of fixed, and is in the bottom right corner. | +| `--right` | `1rem` | By default the widget has a position of fixed, and is in the bottom right corner. | +| `--top` | `auto` | By default the widget has a position of fixed, and is in the bottom right corner. | +| `--left` | `auto` | By default the widget has a position of fixed, and is in the bottom right corner. | +| `--z-index` | `100000` | The z-index of the widget | +| `--font-family` | `"'Helvetica Neue', Arial, sans-serif"` | Default font-family to use| +| `--font-size` | `14px` | Font size | +### Event Callbacks +Sometimes it’s important to know when someone has started to interact with the feedback form, so you can add custom logging, or start/stop background timers on the page until the user is done. -## Configuration +Pass these callbacks when you initialize the Feedback integration: -### General Integration Configuration +```javascript +new Feedback({ + onActorClick: () => {}, + onDialogOpen: () => {}, + onDialogClose: () => {}, + onSubmitSuccess: () => {}, + onSubmitError: () => {}, +}); +``` -The following options can be configured as options to the integration, in `new Feedback({})`: +## Further Customization +There are two more methods in the integration that can help customization. -| key | type | default | description | -| --------- | ------- | ------- | ----------- | -| tbd | boolean | `true` | tbd | +### Bring Your Own Button +You can skip the default widget button and use your own button. Call `feedback.attachTo()` to have the SDK attach a click listener to your own button. You can additionally supply the same customization options that the constructor accepts (e.g. for text labels and colors). +```javascript +const feedback = new Feedback({ + // Disable injecting the default widget + autoInject: false, +}); + +feedback.attachTo(document.querySelector('#your-button'), { + formTitle: "Report a Bug!" +}); +``` -## Manually Sending Feedback Data +### Bring Your Own Widget -Connect your own feedback UI to Sentry's You can use `feedback.flush()` to immediately send all currently captured feedback data. -When Feedback is currently in buffering mode, this will send up to the last 60 seconds of feedback data, -and also continue sending afterwards, similar to when an error happens & is recorded. +You can also bring your own widget and UI and simply pass a feedback object to the `sendFeedback()` function. + +```html +
+ +