diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c74ac68f4e..f5e6b5cde7f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.64.0 + +- feat(core): Add setMeasurement export (#8791) +- fix(nextjs): Check for existence of default export when wrapping pages (#8794) +- fix(nextjs): Ensure imports are valid relative paths (#8799) +- fix(nextjs): Only re-export default export if it exists (#8800) + ## 7.63.0 - build(deps): bump @opentelemetry/instrumentation from 0.41.0 to 0.41.2 diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 9dbe7f977d7e..4ca6c7352261 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -29,6 +29,7 @@ export { export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; export { addTracingExtensions, + setMeasurement, extractTraceparentData, getActiveTransaction, spanStatusfromHttpCode, diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 7a1f5336fe1c..f418453ff28d 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -8,3 +8,4 @@ export { SpanStatus } from './spanstatus'; export type { SpanStatusType } from './span'; export { trace } from './trace'; export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; +export { setMeasurement } from './measurement'; diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts new file mode 100644 index 000000000000..b13bcb6b5a4a --- /dev/null +++ b/packages/core/src/tracing/measurement.ts @@ -0,0 +1,13 @@ +import type { MeasurementUnit } from '@sentry/types'; + +import { getActiveTransaction } from './utils'; + +/** + * Adds a measurement to the current active transaction. + */ +export function setMeasurement(name: string, value: number, unit: MeasurementUnit): void { + const transaction = getActiveTransaction(); + if (transaction) { + transaction.setMeasurement(name, value, unit); + } +} diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 1340ea3db55c..4301391ff1da 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -152,6 +152,9 @@ export class Span implements SpanInterface { if (spanContext.description) { this.description = spanContext.description; } + if (spanContext.name) { + this.description = spanContext.name; + } if (spanContext.data) { this.data = spanContext.data; } @@ -243,6 +246,13 @@ export class Span implements SpanInterface { return this; } + /** + * @inheritDoc + */ + public setName(name: string): void { + this.description = name; + } + /** * @inheritDoc */ diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 9a901df3d316..ee0a5724b9bf 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -44,6 +44,9 @@ export class Transaction extends SpanClass implements TransactionInterface { */ public constructor(transactionContext: TransactionContext, hub?: Hub) { super(transactionContext); + // We need to delete description since it's set by the Span class constructor + // but not needed for transactions. + delete this.description; this._measurements = {}; this._contexts = {}; diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index f2e07ab6537d..57b913b23ab1 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -96,7 +96,12 @@ export default function wrappingLoader( const sentryConfigImportPath = path .relative(path.dirname(this.resourcePath), sentryConfigFilePath) .replace(/\\/g, '/'); - templateCode = templateCode.replace(/__SENTRY_CONFIG_IMPORT_PATH__/g, sentryConfigImportPath); + + // path.relative() may return something like `sentry.server.config.js` which is not allowed. Imports from the + // current directory need to start with './'.This is why we prepend the path with './', which should always again + // be a valid relative path. + // https://github.com/getsentry/sentry-javascript/issues/8798 + templateCode = templateCode.replace(/__SENTRY_CONFIG_IMPORT_PATH__/g, `./${sentryConfigImportPath}`); } else { // Bail without doing any wrapping this.callback(null, userCode, userModuleSourceMap); diff --git a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts index 12b8429bc7d4..b98087aec142 100644 --- a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -14,18 +14,18 @@ import * as Sentry from '@sentry/nextjs'; import type { GetServerSideProps, GetStaticProps, NextPage as NextPageComponent } from 'next'; type NextPageModule = { - default: { getInitialProps?: NextPageComponent['getInitialProps'] }; + default?: { getInitialProps?: NextPageComponent['getInitialProps'] }; getStaticProps?: GetStaticProps; getServerSideProps?: GetServerSideProps; }; const userPageModule = wrapee as NextPageModule; -const pageComponent = userPageModule.default; +const pageComponent = userPageModule ? userPageModule.default : undefined; -const origGetInitialProps = pageComponent.getInitialProps; -const origGetStaticProps = userPageModule.getStaticProps; -const origGetServerSideProps = userPageModule.getServerSideProps; +const origGetInitialProps = pageComponent ? pageComponent.getInitialProps : undefined; +const origGetStaticProps = userPageModule ? userPageModule.getStaticProps : undefined; +const origGetServerSideProps = userPageModule ? userPageModule.getServerSideProps : undefined; const getInitialPropsWrappers: Record = { '/_app': Sentry.wrapAppGetInitialPropsWithSentry, @@ -35,7 +35,7 @@ const getInitialPropsWrappers: Record = { const getInitialPropsWrapper = getInitialPropsWrappers['__ROUTE__'] || Sentry.wrapGetInitialPropsWithSentry; -if (typeof origGetInitialProps === 'function') { +if (pageComponent && typeof origGetInitialProps === 'function') { pageComponent.getInitialProps = getInitialPropsWrapper(origGetInitialProps) as NextPageComponent['getInitialProps']; } diff --git a/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts b/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts index 1720c3b62672..ab38854f090f 100644 --- a/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts @@ -4,8 +4,14 @@ import '__SENTRY_CONFIG_IMPORT_PATH__'; // @ts-ignore This is the file we're wrapping // eslint-disable-next-line import/no-unresolved -export * from '__SENTRY_WRAPPING_TARGET_FILE__'; +import * as wrappee from '__SENTRY_WRAPPING_TARGET_FILE__'; + +// @ts-ignore default either exists, or it doesn't - we don't care +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +const defaultExport = wrappee.default; // @ts-ignore This is the file we're wrapping // eslint-disable-next-line import/no-unresolved -export { default } from '__SENTRY_WRAPPING_TARGET_FILE__'; +export * from '__SENTRY_WRAPPING_TARGET_FILE__'; + +export default defaultExport; 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 654e61c6a5e1..040f3890fc11 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 @@ -8,9 +8,21 @@ conditionalTest({ min: 12 })('Prisma ORM Integration', () => { assertSentryTransaction(envelope[2], { transaction: 'Test Transaction', spans: [ - { description: 'User create', op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, - { description: 'User findMany', op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, - { description: 'User deleteMany', op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, + { + description: 'User create', + op: 'db.sql.prisma', + data: { 'db.system': 'postgresql', 'db.operation': 'create', 'db.prisma.version': '3.12.0' }, + }, + { + description: 'User findMany', + op: 'db.sql.prisma', + data: { 'db.system': 'postgresql', 'db.operation': 'findMany', 'db.prisma.version': '3.12.0' }, + }, + { + description: 'User deleteMany', + op: 'db.sql.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 654e61c6a5e1..040f3890fc11 100644 --- a/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/test.ts @@ -8,9 +8,21 @@ conditionalTest({ min: 12 })('Prisma ORM Integration', () => { assertSentryTransaction(envelope[2], { transaction: 'Test Transaction', spans: [ - { description: 'User create', op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, - { description: 'User findMany', op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, - { description: 'User deleteMany', op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, + { + description: 'User create', + op: 'db.sql.prisma', + data: { 'db.system': 'postgresql', 'db.operation': 'create', 'db.prisma.version': '3.12.0' }, + }, + { + description: 'User findMany', + op: 'db.sql.prisma', + data: { 'db.system': 'postgresql', 'db.operation': 'findMany', 'db.prisma.version': '3.12.0' }, + }, + { + description: 'User deleteMany', + op: 'db.sql.prisma', + data: { 'db.system': 'postgresql', 'db.operation': 'deleteMany', 'db.prisma.version': '3.12.0' }, + }, ], }); }); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 0031a587c602..e0443691a8ae 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -54,6 +54,7 @@ export { trace, withScope, captureCheckIn, + setMeasurement, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index f6ae3a2b9184..99730ac8dac1 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -49,4 +49,5 @@ export { deepReadDirSync, Handlers, Integrations, + setMeasurement, } from '@sentry/node'; diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 3641993149bf..96f43cc9f7f9 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -44,6 +44,7 @@ export { deepReadDirSync, Integrations, Handlers, + setMeasurement, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index ffe1db0e9e75..eb0276b7f95d 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -201,7 +201,7 @@ describe('handleSentry', () => { expect(ref.spanRecorder.spans).toHaveLength(2); expect(ref.spanRecorder.spans).toEqual( expect.arrayContaining([ - expect.objectContaining({ op: 'http.server', description: 'GET /users/[id]' }), + expect.objectContaining({ op: 'http.server', name: 'GET /users/[id]' }), expect.objectContaining({ op: 'http.server', description: 'GET api/users/details/[id]' }), ]), ); diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index c87562dca98b..b8cb6ebff7b3 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -36,6 +36,10 @@ type PrismaMiddleware = ( interface PrismaClient { _sentryInstrumented?: boolean; + _engineConfig?: { + activeProvider?: string; + clientVersion?: string; + }; $use: (cb: PrismaMiddleware) => void; } @@ -70,6 +74,22 @@ export class Prisma implements Integration { // eslint-disable-next-line @typescript-eslint/no-explicit-any addNonEnumerableProperty(options.client as any, '_sentryInstrumented', true); + const clientData: Record = {}; + try { + const engineConfig = (options.client as PrismaClient)._engineConfig; + if (engineConfig) { + const { activeProvider, clientVersion } = engineConfig; + if (activeProvider) { + clientData['db.system'] = activeProvider; + } + if (clientVersion) { + clientData['db.prisma.version'] = clientVersion; + } + } + } catch (e) { + // ignore + } + options.client.$use((params, next: (params: PrismaMiddlewareParams) => Promise) => { if (shouldDisableAutoInstrumentation(getCurrentHub)) { return next(params); @@ -77,8 +97,13 @@ export class Prisma implements Integration { const action = params.action; const model = params.model; + return trace( - { name: model ? `${model} ${action}` : action, op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, + { + name: model ? `${model} ${action}` : action, + op: 'db.sql.prisma', + data: { ...clientData, 'db.operation': action }, + }, () => next(params), ); }); diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts index 61c0e5fb07f6..43d7fe6b5570 100644 --- a/packages/tracing/test/integrations/node/prisma.test.ts +++ b/packages/tracing/test/integrations/node/prisma.test.ts @@ -25,6 +25,11 @@ class PrismaClient { create: () => this._middleware?.({ action: 'create', model: 'user' }, () => Promise.resolve('result')), }; + public _engineConfig = { + activeProvider: 'postgresql', + clientVersion: '3.1.2', + }; + private _middleware?: PrismaMiddleware; constructor() { @@ -48,7 +53,11 @@ describe('setupOnce', function () { void prismaClient.user.create()?.then(() => { expect(mockTrace).toHaveBeenCalledTimes(1); expect(mockTrace).toHaveBeenLastCalledWith( - { name: 'user create', op: 'db.sql.prisma', data: { 'db.system': 'prisma' } }, + { + name: 'user create', + op: 'db.sql.prisma', + data: { 'db.system': 'postgresql', 'db.prisma.version': '3.1.2', 'db.operation': 'create' }, + }, expect.any(Function), ); done(); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 1720dd4e6ec4..e6e35e105508 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -82,6 +82,13 @@ describe('Span', () => { span.setData('foo', true); expect(span.data.foo).toBe(true); }); + + test('setName', () => { + const span = new Span({}); + expect(span.description).toBeUndefined(); + span.setName('foo'); + expect(span.description).toBe('foo'); + }); }); describe('status', () => { diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 756a6808910c..a689590c499e 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -9,6 +9,11 @@ export interface SpanContext { */ description?: string; + /** + * Human-readable identifier for the span. Alias for span.description. + */ + name?: string; + /** * Operation of the Span. */ @@ -139,6 +144,11 @@ export interface Span extends SpanContext { */ setHttpStatus(httpStatus: number): this; + /** + * Set the name of the span. + */ + setName(name: string): void; + /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * Also the `sampled` decision will be inherited. diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index 42d266abbda7..3e1ad24d4669 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -42,7 +42,7 @@ export type TraceparentData = Pick { /** * @inheritDoc */