From 7da30a72628cfd753ff1d81180ca93fba21d3d8d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 25 Jan 2024 15:59:53 +0100 Subject: [PATCH 01/39] chore(biome): Ignore SvelteKit and NextJS build output directories (#10344) --- biome.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 8d1b11d84859..a795c92a22df 100644 --- a/biome.json +++ b/biome.json @@ -48,7 +48,9 @@ "dev-packages/browser-integration-tests/loader-suites/**/*.js", "dev-packages/browser-integration-tests/suites/stacktraces/**/*.js", "**/fixtures/*/*.json", - "**/*.min.js" + "**/*.min.js", + ".next/**", + ".svelte-kit/**" ] }, "javascript": { From 5aac3a68ec019f017dee1b4f25e33c1550983156 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 25 Jan 2024 16:11:38 +0100 Subject: [PATCH 02/39] feat(core): Deprecate `StartSpanOptions.origin` in favour of passing attribute (#10274) To remove ambiguity and normalization logic in v8, we should only set the origin as an attribute, not as a top level option in the new startSpan APIs. --- packages/astro/src/server/middleware.ts | 6 ++-- packages/astro/test/server/middleware.test.ts | 8 +++-- packages/bun/src/integrations/bunserver.ts | 5 ++- packages/core/src/tracing/span.ts | 11 +++--- packages/core/src/tracing/trace.ts | 8 +++-- packages/core/test/lib/tracing/trace.test.ts | 34 +++++++++++++++++++ packages/ember/addon/index.ts | 6 ++-- .../sentry-performance.ts | 13 +++++-- .../src/common/utils/edgeWrapperUtils.ts | 7 ++-- .../src/common/wrapApiHandlerWithSentry.ts | 3 +- .../wrapGenerationFunctionWithSentry.ts | 3 +- .../common/wrapServerComponentWithSentry.ts | 3 +- .../nextjs/test/config/withSentry.test.ts | 4 +-- .../nextjs/test/edge/edgeWrapperUtils.test.ts | 2 +- .../nextjs/test/edge/withSentryAPI.test.ts | 6 ++-- packages/remix/src/utils/instrumentServer.ts | 5 ++- packages/serverless/src/awslambda.ts | 4 +-- packages/serverless/src/awsservices.ts | 5 ++- .../src/gcpfunction/cloud_events.ts | 8 +++-- packages/serverless/src/gcpfunction/events.ts | 8 +++-- packages/serverless/src/gcpfunction/http.ts | 9 +++-- packages/serverless/src/google-cloud-grpc.ts | 5 ++- packages/serverless/src/google-cloud-http.ts | 5 ++- packages/serverless/test/awslambda.test.ts | 18 +++++----- packages/serverless/test/awsservices.test.ts | 13 +++++-- packages/serverless/test/gcpfunction.test.ts | 32 ++++++++--------- .../serverless/test/google-cloud-grpc.test.ts | 5 ++- .../serverless/test/google-cloud-http.test.ts | 9 +++-- packages/sveltekit/src/client/load.ts | 6 ++-- packages/sveltekit/src/server/handle.ts | 12 +++++-- packages/sveltekit/src/server/load.ts | 12 ++++--- packages/sveltekit/test/client/load.test.ts | 9 +++-- packages/sveltekit/test/server/load.test.ts | 26 ++++++++++---- .../tracing-internal/src/browser/request.ts | 3 +- packages/tracing-internal/src/common/fetch.ts | 3 +- .../src/node/integrations/prisma.ts | 6 ++-- packages/tracing/test/hub.test.ts | 4 +-- .../test/integrations/node/prisma.test.ts | 4 ++- 38 files changed, 233 insertions(+), 97 deletions(-) diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index d5cc61b73e95..a66c942076b1 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { captureException, continueTrace, @@ -119,9 +119,11 @@ async function instrumentRequest( const res = await startSpan( { ...traceCtx, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro', + }, name: `${method} ${interpolatedRoute || ctx.url.pathname}`, op: 'http.server', - origin: 'auto.http.astro', status: 'ok', metadata: { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 9fa5bc430c90..c641f5ac6177 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -58,6 +58,9 @@ describe('sentryMiddleware', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + attributes: { + 'sentry.origin': 'auto.http.astro', + }, data: { method: 'GET', url: 'https://mydomain.io/users/123/details', @@ -66,7 +69,6 @@ describe('sentryMiddleware', () => { metadata: {}, name: 'GET /users/[id]/details', op: 'http.server', - origin: 'auto.http.astro', status: 'ok', }, expect.any(Function), // the `next` function @@ -94,6 +96,9 @@ describe('sentryMiddleware', () => { expect(startSpanSpy).toHaveBeenCalledWith( { + attributes: { + 'sentry.origin': 'auto.http.astro', + }, data: { method: 'GET', url: 'http://localhost:1234/a%xx', @@ -102,7 +107,6 @@ describe('sentryMiddleware', () => { metadata: {}, name: 'GET a%xx', op: 'http.server', - origin: 'auto.http.astro', status: 'ok', }, expect.any(Function), // the `next` function diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index fb12cf94432b..aa8765638647 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, captureException, @@ -69,9 +70,11 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] ctx => { return startSpan( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve', + }, op: 'http.server', name: `${request.method} ${parsedUrl.path || '/'}`, - origin: 'auto.http.bun.serve', ...ctx, data, metadata: { diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 8ddfddb3b353..a52d6bd5e9c8 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -130,11 +130,15 @@ export class Span implements SpanInterface { this.tags = spanContext.tags ? { ...spanContext.tags } : {}; // eslint-disable-next-line deprecation/deprecation this.data = spanContext.data ? { ...spanContext.data } : {}; - this._attributes = spanContext.attributes ? { ...spanContext.attributes } : {}; // eslint-disable-next-line deprecation/deprecation this.instrumenter = spanContext.instrumenter || 'sentry'; - this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanContext.origin || 'manual'); + this._attributes = {}; + this.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanContext.origin || 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: spanContext.op, + ...spanContext.attributes, + }); // eslint-disable-next-line deprecation/deprecation this._name = spanContext.name || spanContext.description; @@ -146,9 +150,6 @@ export class Span implements SpanInterface { if ('sampled' in spanContext) { this._sampled = spanContext.sampled; } - if (spanContext.op) { - this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, spanContext.op); - } if (spanContext.status) { this._status = spanContext.status; } diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index eb070510a3b7..bb92373d3e58 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -20,7 +20,7 @@ import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; -interface StartSpanOptions extends TransactionContext { +interface StartSpanOptions extends Omit { /** A manually specified start time for the created `Span` object. */ startTime?: SpanTimeInput; @@ -33,7 +33,11 @@ interface StartSpanOptions extends TransactionContext { /** An op for the span. This is a categorization for spans. */ op?: string; - /** The origin of the span - if it comes from auto instrumenation or manual instrumentation. */ + /** + * The origin of the span - if it comes from auto instrumentation or manual instrumentation. + * + * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead. + */ origin?: SpanOrigin; /** Attributes for the span. */ diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index a2bdac43b6dd..cc11793fc07e 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -229,6 +229,40 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans).toHaveLength(2); expect(spanToJSON(ref.spanRecorder.spans[1]).op).toEqual('db.query'); }); + + it.each([ + { origin: 'auto.http.browser' }, + { attributes: { 'sentry.origin': 'auto.http.browser' } }, + // attribute should take precedence over top level origin + { origin: 'manual', attributes: { 'sentry.origin': 'auto.http.browser' } }, + ])('correctly sets the span origin', async () => { + let ref: any = undefined; + client.on('finishTransaction', transaction => { + ref = transaction; + }); + try { + await startSpan({ name: 'GET users/[id]', origin: 'auto.http.browser' }, () => { + return callback(); + }); + } catch (e) { + // + } + + const jsonSpan = spanToJSON(ref); + expect(jsonSpan).toEqual({ + data: { + 'sentry.origin': 'auto.http.browser', + 'sentry.sample_rate': 0, + }, + origin: 'auto.http.browser', + description: 'GET users/[id]', + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: isError ? 'internal_error' : undefined, + timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); }); it('creates & finishes span', async () => { diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 4f1f399654e4..b3ccfffa404f 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -5,7 +5,7 @@ import { getOwnConfig, isDevelopingApp, macroCondition } from '@embroider/macros import { startSpan } from '@sentry/browser'; import type { BrowserOptions } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; -import { applySdkMetadata } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata } from '@sentry/core'; import { GLOBAL_OBJ } from '@sentry/utils'; import Ember from 'ember'; @@ -82,9 +82,11 @@ export const instrumentRoutePerformance = (BaseRoute ): Promise> => { return startSpan( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'ember', + }, op, name: description, - origin: 'auto.ui.ember', }, () => { return fn(...args); diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index b25125b28da6..acabe5334cad 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -11,6 +11,7 @@ import type { ExtendedBackburner } from '@sentry/ember/runloop'; import type { Span, Transaction } from '@sentry/types'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { BrowserClient } from '..'; import { getActiveSpan, startInactiveSpan } from '..'; import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig, StartTransactionFunction } from '../types'; @@ -150,9 +151,11 @@ export function _instrumentEmberRouter( }, }); transitionSpan = startInactiveSpan({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + }, op: 'ui.ember.transition', name: `route:${fromRoute} -> route:${toRoute}`, - origin: 'auto.ui.ember', }); }); @@ -212,9 +215,11 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { if ((now - currentQueueStart) * 1000 >= minQueueDuration) { startInactiveSpan({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + }, name: 'runloop', op: `ui.ember.runloop.${queue}`, - origin: 'auto.ui.ember', startTimestamp: currentQueueStart, })?.end(now); } @@ -370,7 +375,9 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { startInactiveSpan({ op: 'ui.ember.init', name: 'init', - origin: 'auto.ui.ember', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + }, startTimestamp, })?.end(endTimestamp); performance.clearMarks(startName); diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 59114ddee709..109a586d7cd6 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, @@ -40,8 +41,10 @@ export function withEdgeWrapping( ...transactionContext, name: options.spanDescription, op: options.spanOp, - origin: 'auto.function.nextjs.withEdgeWrapping', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', + }, metadata: { // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index 16228aa0cda8..91c95e264875 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -9,6 +9,7 @@ import { } from '@sentry/core'; import { consoleSandbox, isString, logger, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler } from './types'; import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; import { flushQueue } from './utils/responseEnd'; @@ -108,9 +109,9 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri ...transactionContext, name: `${reqMethod}${reqPath}`, op: 'http.server', - origin: 'auto.http.nextjs', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', }, metadata: { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index f2e829704dd6..5e6a051ffcfb 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -12,6 +12,7 @@ import { import type { WebFetchHeaders } from '@sentry/types'; import { winterCGHeadersToDict } from '@sentry/utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { GenerationFunctionContext } from '../common/types'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { commonObjectToPropagationContext } from './utils/commonObjectTracing'; @@ -67,11 +68,11 @@ export function wrapGenerationFunctionWithSentry a { op: 'function.nextjs', name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, - origin: 'auto.function.nextjs', ...transactionContext, data, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', }, metadata: { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index a0a1ae2f77aa..f8b6c5698550 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -10,6 +10,7 @@ import { } from '@sentry/core'; import { winterCGHeadersToDict } from '@sentry/utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; import { commonObjectToPropagationContext } from './utils/commonObjectTracing'; @@ -61,9 +62,9 @@ export function wrapServerComponentWithSentry any> op: 'function.nextjs', name: `${componentType} Server Component (${componentRoute})`, status: 'ok', - origin: 'auto.function.nextjs', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', }, metadata: { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index 91b61516a240..1ae933549b17 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,5 +1,5 @@ import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core'; import type { NextApiRequest, NextApiResponse } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/common/types'; @@ -44,9 +44,9 @@ describe('withSentry', () => { { name: 'GET http://dogs.are.great', op: 'http.server', - origin: 'auto.http.nextjs', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', }, metadata: { request: expect.objectContaining({ url: 'http://dogs.are.great' }), diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts index 97d6e7b103e1..495e6336e2cd 100644 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts @@ -87,10 +87,10 @@ describe('withEdgeWrapping', () => { }, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', }, name: 'some label', op: 'some op', - origin: 'auto.function.nextjs.withEdgeWrapping', }), expect.any(Function), ); diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index ea5e7c4319f0..071bdda93952 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -1,5 +1,5 @@ import * as coreSdk from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { wrapApiHandlerWithSentry } from '../../src/edge'; @@ -58,10 +58,10 @@ describe('wrapApiHandlerWithSentry', () => { }, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', }, name: 'POST /user/[userId]/post/[postId]', op: 'http.server', - origin: 'auto.function.nextjs.withEdgeWrapping', }), expect.any(Function), ); @@ -80,10 +80,10 @@ describe('wrapApiHandlerWithSentry', () => { metadata: {}, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', }, name: 'handler (/user/[userId]/post/[postId])', op: 'http.server', - origin: 'auto.function.nextjs.withEdgeWrapping', }), expect.any(Function), ); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 22171153a534..f90e9dfabd77 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan, getActiveTransaction, getClient, @@ -411,7 +412,9 @@ export function startRequestHandlerTransaction( const transaction = hub.startTransaction({ name, op: 'http.server', - origin: 'auto.http.remix', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix', + }, tags: { method: request.method, }, diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index e51ecd56fa4d..89086e7a1c77 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -23,7 +23,7 @@ import { isString, logger } from '@sentry/utils'; import type { Context, Handler } from 'aws-lambda'; import { performance } from 'perf_hooks'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { AWSServices } from './awsservices'; import { DEBUG_BUILD } from './debug-build'; import { markEventUnhandled } from './utils'; @@ -361,10 +361,10 @@ export function wrapHandler( { name: context.functionName, op: 'function.aws.lambda', - origin: 'auto.function.serverless', ...continueTraceContext, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, }, span => { diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 8992b7a7adb0..36a789c52632 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { startInactiveSpan } from '@sentry/node'; import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; @@ -62,7 +63,9 @@ function wrapMakeRequest( span = startInactiveSpan({ name: describe(this, operation, params), op: 'http.client', - origin: 'auto.http.serverless', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, }); }); req.on('complete', () => { diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 92a3eb0e37e7..533c74bb7653 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -35,8 +35,10 @@ function _wrapCloudEventFunction( { name: context.type || '', op: 'function.gcp.cloud_event', - origin: 'auto.function.serverless.gcp_cloud_event', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 79c609e9108c..501b6f7d6da3 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -38,8 +38,10 @@ function _wrapEventFunction { name: context.eventType, op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 41fa620779c7..84e3627477cd 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,4 +1,9 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, handleCallbackErrors } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + Transaction, + handleCallbackErrors, +} from '@sentry/core'; import type { AddRequestDataToEventOptions } from '@sentry/node'; import { continueTrace, startSpanManual } from '@sentry/node'; import { getCurrentScope } from '@sentry/node'; @@ -78,9 +83,9 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 458af78872f7..ba2ddb038a03 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -1,4 +1,5 @@ import type { EventEmitter } from 'events'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { startInactiveSpan } from '@sentry/node'; import type { Integration } from '@sentry/types'; import { fill } from '@sentry/utils'; @@ -110,7 +111,9 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str const span = startInactiveSpan({ name: `${callType} ${methodName}`, op: `grpc.${serviceIdentifier}`, - origin: 'auto.grpc.serverless', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', + }, }); ret.on('status', () => { if (span) { diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 369fa6ad230d..769519013785 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -1,6 +1,7 @@ // '@google-cloud/common' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import type * as common from '@google-cloud/common'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { startInactiveSpan } from '@sentry/node'; import type { Integration } from '@sentry/types'; import { fill } from '@sentry/utils'; @@ -55,7 +56,9 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { const span = startInactiveSpan({ name: `${httpMethod} ${reqOpts.uri}`, op: `http.client.${identifyService(this.apiEndpoint)}`, - origin: 'auto.http.serverless', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, }); orig.call(this, reqOpts, (...args: Parameters) => { if (span) { diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 0d923074067f..de851cda8bbb 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,5 +1,5 @@ // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Event } from '@sentry/types'; import type { Callback, Handler } from 'aws-lambda'; @@ -206,9 +206,9 @@ describe('AWSLambda', () => { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - origin: 'auto.function.serverless', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: {}, }; @@ -236,9 +236,9 @@ describe('AWSLambda', () => { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - origin: 'auto.function.serverless', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: {}, }; @@ -278,11 +278,11 @@ describe('AWSLambda', () => { parentSpanId: '1121201211212012', parentSampled: false, op: 'function.aws.lambda', - origin: 'auto.function.serverless', name: 'functionName', traceId: '12312012123120121231201212312012', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: { dynamicSamplingContext: { @@ -316,12 +316,12 @@ describe('AWSLambda', () => { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - origin: 'auto.function.serverless', traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: { dynamicSamplingContext: {} }, }; @@ -349,9 +349,9 @@ describe('AWSLambda', () => { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - origin: 'auto.function.serverless', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: {}, }; @@ -390,9 +390,9 @@ describe('AWSLambda', () => { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - origin: 'auto.function.serverless', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: {}, }; @@ -435,9 +435,9 @@ describe('AWSLambda', () => { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - origin: 'auto.function.serverless', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: {}, }; @@ -476,9 +476,9 @@ describe('AWSLambda', () => { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', - origin: 'auto.function.serverless', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless', }, metadata: {}, }; diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts index 16464e315fa6..b18b1d8dd9af 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/serverless/test/awsservices.test.ts @@ -2,6 +2,7 @@ import * as SentryNode from '@sentry/node'; import * as AWS from 'aws-sdk'; import * as nock from 'nock'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { AWSServices } from '../src/awsservices'; describe('AWSServices', () => { @@ -25,7 +26,9 @@ describe('AWSServices', () => { expect(data.Body?.toString('utf-8')).toEqual('contents'); expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client', - origin: 'auto.http.serverless', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, name: 'aws.s3.getObject foo', }); // @ts-expect-error see "Why @ts-expect-error" note @@ -42,8 +45,10 @@ describe('AWSServices', () => { }); expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client', - origin: 'auto.http.serverless', name: 'aws.s3.getObject foo', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, }); }); }); @@ -57,7 +62,9 @@ describe('AWSServices', () => { expect(data.Payload?.toString('utf-8')).toEqual('reply'); expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client', - origin: 'auto.http.serverless', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, name: 'aws.lambda.invoke foo', }); }); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 29cfe0541a0c..8fb51d3bf368 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -2,7 +2,7 @@ import * as domain from 'domain'; import * as SentryNode from '@sentry/node'; import type { Event, Integration } from '@sentry/types'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as Sentry from '../src'; import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction'; import type { @@ -105,9 +105,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'POST /path', op: 'function.gcp.http', - origin: 'auto.function.serverless.gcp_http', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', }, metadata: {}, }; @@ -135,12 +135,12 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'POST /path', op: 'function.gcp.http', - origin: 'auto.function.serverless.gcp_http', traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', }, metadata: { dynamicSamplingContext: { @@ -168,12 +168,12 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'POST /path', op: 'function.gcp.http', - origin: 'auto.function.serverless.gcp_http', traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', }, metadata: { dynamicSamplingContext: {} }, }; @@ -253,9 +253,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', }, }; @@ -276,9 +276,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', }, }; @@ -304,9 +304,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', }, }; @@ -331,9 +331,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', }, }; @@ -356,9 +356,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', }, }; @@ -379,9 +379,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', }, }; @@ -403,9 +403,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.event', - origin: 'auto.function.serverless.gcp_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', }, }; @@ -460,9 +460,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.cloud_event', - origin: 'auto.function.serverless.gcp_cloud_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', }, }; @@ -483,9 +483,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.cloud_event', - origin: 'auto.function.serverless.gcp_cloud_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', }, }; @@ -508,9 +508,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.cloud_event', - origin: 'auto.function.serverless.gcp_cloud_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', }, }; @@ -531,9 +531,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.cloud_event', - origin: 'auto.function.serverless.gcp_cloud_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', }, }; @@ -555,9 +555,9 @@ describe('GCPFunction', () => { const fakeTransactionContext = { name: 'event.type', op: 'function.gcp.cloud_event', - origin: 'auto.function.serverless.gcp_cloud_event', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', }, }; diff --git a/packages/serverless/test/google-cloud-grpc.test.ts b/packages/serverless/test/google-cloud-grpc.test.ts index 39ebb4a54ecd..8c0e0866bf0c 100644 --- a/packages/serverless/test/google-cloud-grpc.test.ts +++ b/packages/serverless/test/google-cloud-grpc.test.ts @@ -9,6 +9,7 @@ import * as SentryNode from '@sentry/node'; import * as http2 from 'http2'; import * as nock from 'nock'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GoogleCloudGrpc } from '../src/google-cloud-grpc'; const spyConnect = jest.spyOn(http2, 'connect'); @@ -121,7 +122,9 @@ describe('GoogleCloudGrpc tracing', () => { expect(resp).toEqual('1637084156623860'); expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'grpc.pubsub', - origin: 'auto.grpc.serverless', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', + }, name: 'unary call publish', }); await pubsub.close(); diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts index 0ef1466647a5..748841e58579 100644 --- a/packages/serverless/test/google-cloud-http.test.ts +++ b/packages/serverless/test/google-cloud-http.test.ts @@ -4,6 +4,7 @@ import { BigQuery } from '@google-cloud/bigquery'; import * as SentryNode from '@sentry/node'; import * as nock from 'nock'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GoogleCloudHttp } from '../src/google-cloud-http'; describe('GoogleCloudHttp tracing', () => { @@ -52,13 +53,17 @@ describe('GoogleCloudHttp tracing', () => { expect(resp).toEqual([[{ foo: true }]]); expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', - origin: 'auto.http.serverless', name: 'POST /jobs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, }); expect(SentryNode.startInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', - origin: 'auto.http.serverless', name: expect.stringMatching(/^GET \/queries\/.+/), + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, }); }); }); diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index 545105e49c43..14528959d34e 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors, startSpan } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, handleCallbackErrors, startSpan } from '@sentry/core'; import { captureException } from '@sentry/svelte'; import { addNonEnumerableProperty, objectify } from '@sentry/utils'; import type { LoadEvent } from '@sveltejs/kit'; @@ -80,7 +80,9 @@ export function wrapLoadWithSentry any>(origLoad: T) return startSpan( { op: 'function.sveltekit.load', - origin: 'auto.function.sveltekit', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, name: routeId ? routeId : event.url.pathname, status: 'ok', metadata: { diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 1bb0c485168e..5572e64060d3 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,4 +1,10 @@ -import { getActiveSpan, getCurrentScope, getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + getActiveSpan, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ @@ -169,7 +175,9 @@ async function instrumentHandle( const resolveResult = await startSpan( { op: 'http.server', - origin: 'auto.http.sveltekit', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + }, name: `${event.request.method} ${event.route?.id || event.url.pathname}`, status: 'ok', ...traceparentData, diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts index 5d0cd3c1cb90..9728dcf47b5b 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server/load.ts @@ -1,10 +1,10 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ -import { getCurrentScope, startSpan } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getCurrentScope, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; -import type { TransactionContext } from '@sentry/types'; import { addNonEnumerableProperty, objectify } from '@sentry/utils'; import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; +import type { TransactionContext } from '@sentry/types'; import type { SentryWrappedFlag } from '../common/utils'; import { isHttpError, isRedirect } from '../common/utils'; import { flushIfServerless, getTracePropagationData } from './utils'; @@ -67,7 +67,9 @@ export function wrapLoadWithSentry any>(origLoad: T) const traceLoadContext: TransactionContext = { op: 'function.sveltekit.load', - origin: 'auto.function.sveltekit', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, name: routeId ? routeId : event.url.pathname, status: 'ok', metadata: { @@ -134,7 +136,9 @@ export function wrapServerLoadWithSentry any>(origSe const traceLoadContext: TransactionContext = { op: 'function.sveltekit.server.load', - origin: 'auto.function.sveltekit', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, name: routeId ? routeId : event.url.pathname, status: 'ok', metadata: { diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts index 15d2850b4a8c..e839b5a9cba5 100644 --- a/packages/sveltekit/test/client/load.test.ts +++ b/packages/sveltekit/test/client/load.test.ts @@ -3,6 +3,7 @@ import type { Load } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; import { vi } from 'vitest'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { wrapLoadWithSentry } from '../../src/client/load'; const mockCaptureException = vi.spyOn(SentrySvelte, 'captureException').mockImplementation(() => 'xx'); @@ -82,8 +83,10 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.load', - origin: 'auto.function.sveltekit', name: '/users/[id]', status: 'ok', metadata: { @@ -110,8 +113,10 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.load', - origin: 'auto.function.sveltekit', name: '/users/123', status: 'ok', metadata: { diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index 6b86ca6b32f6..2656b22f685a 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, addTracingExtensions } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Load, ServerLoad } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; @@ -197,8 +197,10 @@ describe('wrapLoadWithSentry calls trace', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.load', - origin: 'auto.function.sveltekit', name: '/users/[id]', status: 'ok', metadata: { @@ -216,8 +218,10 @@ describe('wrapLoadWithSentry calls trace', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.load', - origin: 'auto.function.sveltekit', name: '/users/123', status: 'ok', metadata: { @@ -250,8 +254,10 @@ describe('wrapServerLoadWithSentry calls trace', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.server.load', - origin: 'auto.function.sveltekit', name: '/users/[id]', parentSampled: true, parentSpanId: '1234567890abcdef', @@ -284,8 +290,10 @@ describe('wrapServerLoadWithSentry calls trace', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.server.load', - origin: 'auto.function.sveltekit', name: '/users/[id]', status: 'ok', data: { @@ -306,8 +314,10 @@ describe('wrapServerLoadWithSentry calls trace', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.server.load', - origin: 'auto.function.sveltekit', name: '/users/[id]', parentSampled: true, parentSpanId: '1234567890abcdef', @@ -335,8 +345,10 @@ describe('wrapServerLoadWithSentry calls trace', () => { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenCalledWith( { + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + }, op: 'function.sveltekit.server.load', - origin: 'auto.function.sveltekit', name: '/users/123', parentSampled: true, parentSpanId: '1234567890abcdef', diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index c84d0545054b..d2600f02629e 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -282,10 +283,10 @@ export function xhrCallback( type: 'xhr', 'http.method': sentryXhrData.method, url: sentryXhrData.url, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', }, name: `${sentryXhrData.method} ${sentryXhrData.url}`, op: 'http.client', - origin: 'auto.http.browser', }) : undefined; diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index c96778f8cd35..8f9da76488ad 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -85,10 +86,10 @@ export function instrumentFetchRequest( url, type: 'fetch', 'http.method': method, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, }, name: `${method} ${url}`, op: 'http.client', - origin: spanOrigin, }) : undefined; diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index f51bcd6eef32..4399778f1e80 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, startSpan } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getCurrentHub, startSpan } from '@sentry/core'; import type { Integration } from '@sentry/types'; import { addNonEnumerableProperty, logger } from '@sentry/utils'; @@ -104,7 +104,9 @@ export class Prisma implements Integration { { name: model ? `${model} ${action}` : action, op: 'db.prisma', - origin: 'auto.db.prisma', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.prisma', + }, data: { ...clientData, 'db.operation': action }, }, () => next(params), diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index 7b73d9bb2d51..02841dbd5aca 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -305,8 +305,8 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark', parentSampled: true }); - // length 1 because `sentry.origin` is set on span initialization - expect(Transaction.prototype.setAttribute).toHaveBeenCalledTimes(1); + // length 2 because origin and op are set as attributes on span initialization + expect(Transaction.prototype.setAttribute).toHaveBeenCalledTimes(2); }); it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => { diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts index 552edb1b78c2..c86f8aecabe2 100644 --- a/packages/tracing/test/integrations/node/prisma.test.ts +++ b/packages/tracing/test/integrations/node/prisma.test.ts @@ -54,9 +54,11 @@ describe('setupOnce', function () { expect(mockStartSpan).toHaveBeenCalledTimes(1); expect(mockStartSpan).toHaveBeenLastCalledWith( { + attributes: { + 'sentry.origin': 'auto.db.prisma', + }, name: 'user create', op: 'db.prisma', - origin: 'auto.db.prisma', data: { 'db.system': 'postgresql', 'db.prisma.version': '3.1.2', 'db.operation': 'create' }, }, expect.any(Function), From cbccf51dfda3ee4acec07c7ee851cd5dd68aae73 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 25 Jan 2024 16:56:07 +0100 Subject: [PATCH 03/39] ref(types): Add known attribute keys to `SpanAttributes` type (#10295) add known attribute keys to the `SpanAttributes` type because this way we can - enforce a concrete type for some known keys - provide auto suggestions when setting attributes --- packages/types/src/span.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index e34940b35d65..0dc46467269d 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -23,7 +23,13 @@ export type SpanAttributeValue = | Array | Array; -export type SpanAttributes = Record; +export type SpanAttributes = Partial<{ + 'sentry.origin': string; + 'sentry.op': string; + 'sentry.source': string; + 'sentry.sample_rate': number; +}> & + Record; /** This type is aligned with the OpenTelemetry TimeInput type. */ export type SpanTimeInput = HrTime | number | Date; From d64b7981a124f5f1c0e69284a8452a2d8404dfa3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 25 Jan 2024 12:40:00 -0500 Subject: [PATCH 04/39] feat(core): Add `afterAllSetup` hook for integrations (#10345) This adds a new hook to integrations which runs after all integrations have run their `setup` hooks. This can be used to handle integrations that depend on each other. The only timing guarantee is that `afterAllSetup` will be called after every integration has run `setupOnce` and `setup`. --- packages/core/src/baseclient.ts | 6 +++- packages/core/src/integration.ts | 12 ++++++++ packages/core/test/lib/sdk.test.ts | 49 +++++++++++++++++++++++++++++- packages/types/src/integration.ts | 12 ++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index d59d596e0b82..c7736e278c1c 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -53,6 +53,7 @@ import { createEventEnvelope, createSessionEnvelope } from './envelope'; import { getClient } from './exports'; import { getIsolationScope } from './hub'; import type { IntegrationIndex } from './integration'; +import { afterSetupIntegrations } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import { createMetricEnvelope } from './metrics/envelope'; import type { Scope } from './scope'; @@ -532,7 +533,10 @@ export abstract class BaseClient implements Client { /** Setup integrations for this client. */ protected _setupIntegrations(): void { - this._integrations = setupIntegrations(this, this._options.integrations); + const { integrations } = this._options; + this._integrations = setupIntegrations(this, integrations); + afterSetupIntegrations(this, integrations); + // TODO v8: We don't need this flag anymore this._integrationsInitialized = true; } diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 5b126459390b..8a91fa10e303 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -108,6 +108,18 @@ export function setupIntegrations(client: Client, integrations: Integration[]): return integrationIndex; } +/** + * Execute the `afterAllSetup` hooks of the given integrations. + */ +export function afterSetupIntegrations(client: Client, integrations: Integration[]): void { + for (const integration of integrations) { + // guard against empty provided integrations + if (integration && integration.afterAllSetup) { + integration.afterAllSetup(client); + } + } +} + /** Setup a single integration. */ export function setupIntegration(client: Client, integration: Integration, integrationIndex: IntegrationIndex): void { if (integrationIndex[integration.name]) { diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 1484971babf7..c9d18c02c78e 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -1,5 +1,5 @@ import { Hub, captureCheckIn, makeMain, setCurrentClient } from '@sentry/core'; -import type { Client, Integration } from '@sentry/types'; +import type { Client, Integration, IntegrationFnResult } from '@sentry/types'; import { installedIntegrations } from '../../src/integration'; import { initAndBind } from '../../src/sdk'; @@ -35,6 +35,53 @@ describe('SDK', () => { expect((integrations[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); expect((integrations[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); }); + + test('calls hooks in the correct order', () => { + const list: string[] = []; + + const integration1 = { + name: 'integration1', + setupOnce: jest.fn(() => list.push('setupOnce1')), + afterAllSetup: jest.fn(() => list.push('afterAllSetup1')), + } satisfies IntegrationFnResult; + + const integration2 = { + name: 'integration2', + setupOnce: jest.fn(() => list.push('setupOnce2')), + setup: jest.fn(() => list.push('setup2')), + afterAllSetup: jest.fn(() => list.push('afterAllSetup2')), + } satisfies IntegrationFnResult; + + const integration3 = { + name: 'integration3', + setupOnce: jest.fn(() => list.push('setupOnce3')), + setup: jest.fn(() => list.push('setup3')), + } satisfies IntegrationFnResult; + + const integrations: Integration[] = [integration1, integration2, integration3]; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations }); + initAndBind(TestClient, options); + + expect(integration1.setupOnce).toHaveBeenCalledTimes(1); + expect(integration2.setupOnce).toHaveBeenCalledTimes(1); + expect(integration3.setupOnce).toHaveBeenCalledTimes(1); + + expect(integration2.setup).toHaveBeenCalledTimes(1); + expect(integration3.setup).toHaveBeenCalledTimes(1); + + expect(integration1.afterAllSetup).toHaveBeenCalledTimes(1); + expect(integration2.afterAllSetup).toHaveBeenCalledTimes(1); + + expect(list).toEqual([ + 'setupOnce1', + 'setupOnce2', + 'setup2', + 'setupOnce3', + 'setup3', + 'afterAllSetup1', + 'afterAllSetup2', + ]); + }); }); }); diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index 44c49ab375aa..3c3b44eb0ed8 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -39,6 +39,12 @@ export interface IntegrationFnResult { */ setup?(client: Client): void; + /** + * This hook is triggered after `setupOnce()` and `setup()` have been called for all integrations. + * You can use it if it is important that all other integrations have been run before. + */ + afterAllSetup?(client: Client): void; + /** * An optional hook that allows to preprocess an event _before_ it is passed to all other event processors. */ @@ -83,6 +89,12 @@ export interface Integration { */ setup?(client: Client): void; + /** + * This hook is triggered after `setupOnce()` and `setup()` have been called for all integrations. + * You can use it if it is important that all other integrations have been run before. + */ + afterAllSetup?(client: Client): void; + /** * An optional hook that allows to preprocess an event _before_ it is passed to all other event processors. */ From 35b6b2629eb3c8dd941cd26e0ea226f975ab48c9 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 25 Jan 2024 20:12:34 +0100 Subject: [PATCH 05/39] test(node): Add `mysql2` auto instrumentation test for `@sentry/node-experimental` (#10259) This PR adds a test for `mysql2` auto instrumentation for `@sentry/node-experimental`. `mysql2` will not query unless there is a connection and it does not want to work without one like `mysql`. This PR adds a `withDockerCompose` method to the test runner which handles: - Starting the docker container - Waiting until some specific output has been seen from the container so we know the server is up - Closing and cleaning up the docker container/volumes ```ts createRunner(__dirname, 'scenario.js') .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port: 3306'] }) .expect({ transaction: EXPECTED_TRANSACTION }) .start(done); ``` My only minor concern is that the mysql docker container creates a volume to store data. If the cleanup code does not run, a 180MB volume is left behind after every run. This will only be an issue when testing locally but we could start to fill developers machines. These can be cleaned up via `docker volume prune --force` but I would not want to run this on peoples machines without telling them! --- .../node-integration-tests/package.json | 4 +- .../suites/proxy/test.ts | 6 +- .../suites/tracing-experimental/mysql/test.ts | 6 +- .../mysql2/docker-compose.yml | 9 + .../tracing-experimental/mysql2/scenario.js | 34 +++ .../tracing-experimental/mysql2/test.ts | 41 ++++ .../node-integration-tests/utils/runner.ts | 201 ++++++++++++------ yarn.lock | 63 ++++++ 8 files changed, 297 insertions(+), 67 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index ed76bfcbec4f..b0ec56dea204 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -14,7 +14,8 @@ "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", - "clean": "rimraf -g **/node_modules", + "clean": "rimraf -g **/node_modules && run-p clean:docker:*", + "clean:docker:mysql2": "cd suites/tracing-experimental/mysql2 && docker-compose down --volumes", "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", "prisma:init:new": "(cd suites/tracing-new/prisma-orm && ts-node ./setup.ts)", "lint": "eslint . --format stylish", @@ -44,6 +45,7 @@ "mongodb-memory-server-global": "^7.6.3", "mongoose": "^5.13.22", "mysql": "^2.18.1", + "mysql2": "^3.7.1", "nock": "^13.1.0", "pg": "^8.7.3", "proxy": "^2.1.1", diff --git a/dev-packages/node-integration-tests/suites/proxy/test.ts b/dev-packages/node-integration-tests/suites/proxy/test.ts index 5e4619d3948d..dc709f5251c6 100644 --- a/dev-packages/node-integration-tests/suites/proxy/test.ts +++ b/dev-packages/node-integration-tests/suites/proxy/test.ts @@ -1,4 +1,8 @@ -import { createRunner } from '../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); test('proxies sentry requests', done => { createRunner(__dirname, 'basic.js') diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts index 43c67c9c8b07..84c63a30ff68 100644 --- a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql/test.ts @@ -1,7 +1,11 @@ import { conditionalTest } from '../../../utils'; -import { createRunner } from '../../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; conditionalTest({ min: 14 })('mysql auto instrumentation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + test('should auto-instrument `mysql` package when using connection.connect()', done => { const EXPECTED_TRANSACTION = { transaction: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml new file mode 100644 index 000000000000..71ea54ad7e70 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/docker-compose.yml @@ -0,0 +1,9 @@ +services: + db: + image: mysql:8 + restart: always + container_name: integration-tests-mysql + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: password diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js new file mode 100644 index 000000000000..8858e4ef587f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/scenario.js @@ -0,0 +1,34 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node-experimental'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const mysql = require('mysql2/promise'); + +mysql + .createConnection({ + user: 'root', + password: 'password', + host: 'localhost', + port: 3306, + }) + .then(connection => { + return Sentry.startSpan( + { + op: 'transaction', + name: 'Test Transaction', + }, + async _ => { + await connection.query('SELECT 1 + 1 AS solution'); + await connection.query('SELECT NOW()', ['1', '2']); + }, + ); + }); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts new file mode 100644 index 000000000000..28209009b03e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/mysql2/test.ts @@ -0,0 +1,41 @@ +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +conditionalTest({ min: 14 })('mysql2 auto instrumentation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should auto-instrument `mysql` package without connection.connect()', done => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + description: 'SELECT 1 + 1 AS solution', + op: 'db', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'net.peer.name': 'localhost', + 'net.peer.port': 3306, + 'db.user': 'root', + }), + }), + expect.objectContaining({ + description: 'SELECT NOW()', + op: 'db', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'net.peer.name': 'localhost', + 'net.peer.port': 3306, + 'db.user': 'root', + }), + }), + ]), + }; + + createRunner(__dirname, 'scenario.js') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port: 3306'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 31969452ba74..515a2627acf8 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -1,5 +1,4 @@ -import type { ChildProcess } from 'child_process'; -import { spawn } from 'child_process'; +import { spawn, spawnSync } from 'child_process'; import { join } from 'path'; import type { Envelope, EnvelopeItemType, Event, SerializedSession } from '@sentry/types'; import axios from 'axios'; @@ -30,14 +29,17 @@ export function assertSentryTransaction(actual: Event, expected: Partial) }); } -const CHILD_PROCESSES = new Set(); +const CLEANUP_STEPS = new Set(); export function cleanupChildProcesses(): void { - for (const child of CHILD_PROCESSES) { - child.kill(); + for (const step of CLEANUP_STEPS) { + step(); } + CLEANUP_STEPS.clear(); } +process.on('exit', cleanupChildProcesses); + /** Promise only resolves when fn returns true */ async function waitFor(fn: () => boolean, timeout = 10_000): Promise { let remaining = timeout; @@ -50,6 +52,58 @@ async function waitFor(fn: () => boolean, timeout = 10_000): Promise { } } +type VoidFunction = () => void; + +interface DockerOptions { + /** + * The working directory to run docker compose in + */ + workingDirectory: string[]; + /** + * The strings to look for in the output to know that the docker compose is ready for the test to be run + */ + readyMatches: string[]; +} + +/** + * Runs docker compose up and waits for the readyMatches to appear in the output + * + * Returns a function that can be called to docker compose down + */ +async function runDockerCompose(options: DockerOptions): Promise { + return new Promise((resolve, reject) => { + const cwd = join(...options.workingDirectory); + const close = (): void => { + spawnSync('docker', ['compose', 'down', '--volumes'], { cwd }); + }; + + // ensure we're starting fresh + close(); + + const child = spawn('docker', ['compose', 'up'], { cwd }); + + const timeout = setTimeout(() => { + close(); + reject(new Error('Timed out waiting for docker-compose')); + }, 60_000); + + function newData(data: Buffer): void { + const text = data.toString('utf8'); + + for (const match of options.readyMatches) { + if (text.includes(match)) { + child.stdout.removeAllListeners(); + clearTimeout(timeout); + resolve(close); + } + } + } + + child.stdout.on('data', newData); + child.stderr.on('data', newData); + }); +} + type Expected = | { event: Partial | ((event: Event) => void); @@ -70,6 +124,7 @@ export function createRunner(...paths: string[]) { const flags: string[] = []; const ignored: EnvelopeItemType[] = []; let withSentryServer = false; + let dockerOptions: DockerOptions | undefined; let ensureNoErrorOutput = false; if (testPath.endsWith('.ts')) { @@ -93,6 +148,10 @@ export function createRunner(...paths: string[]) { ignored.push(...types); return this; }, + withDockerCompose: function (options: DockerOptions) { + dockerOptions = options; + return this; + }, ensureNoErrorOutput: function () { ensureNoErrorOutput = true; return this; @@ -182,80 +241,94 @@ export function createRunner(...paths: string[]) { ? createBasicSentryServer(newEnvelope) : Promise.resolve(undefined); + const dockerStartup: Promise = dockerOptions + ? runDockerCompose(dockerOptions) + : Promise.resolve(undefined); + + const startup = Promise.all([dockerStartup, serverStartup]); + // eslint-disable-next-line @typescript-eslint/no-floating-promises - serverStartup.then(mockServerPort => { - const env = mockServerPort - ? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } - : process.env; + startup + .then(([dockerChild, mockServerPort]) => { + if (dockerChild) { + CLEANUP_STEPS.add(dockerChild); + } - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN); + const env = mockServerPort + ? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } + : process.env; - child = spawn('node', [...flags, testPath], { env }); + // eslint-disable-next-line no-console + if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN); - CHILD_PROCESSES.add(child); + child = spawn('node', [...flags, testPath], { env }); - if (ensureNoErrorOutput) { - child.stderr.on('data', (data: Buffer) => { - const output = data.toString(); - complete(new Error(`Expected no error output but got: '${output}'`)); + CLEANUP_STEPS.add(() => { + child?.kill(); }); - } - - child.on('close', () => { - hasExited = true; if (ensureNoErrorOutput) { - complete(); + child.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + complete(new Error(`Expected no error output but got: '${output}'`)); + }); } - }); - // Pass error to done to end the test quickly - child.on('error', e => { - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('scenario error', e); - complete(e); - }); - - function tryParseEnvelopeFromStdoutLine(line: string): void { - // Lines can have leading '[something] [{' which we need to remove - const cleanedLine = line.replace(/^.*?] \[{"/, '[{"'); - - // See if we have a port message - if (cleanedLine.startsWith('{"port":')) { - const { port } = JSON.parse(cleanedLine) as { port: number }; - scenarioServerPort = port; - return; - } + child.on('close', () => { + hasExited = true; - // Skip any lines that don't start with envelope JSON - if (!cleanedLine.startsWith('[{')) { - return; - } + if (ensureNoErrorOutput) { + complete(); + } + }); - try { - const envelope = JSON.parse(cleanedLine) as Envelope; - newEnvelope(envelope); - } catch (_) { - // - } - } + // Pass error to done to end the test quickly + child.on('error', e => { + // eslint-disable-next-line no-console + if (process.env.DEBUG) console.log('scenario error', e); + complete(e); + }); - let buffer = Buffer.alloc(0); - child.stdout.on('data', (data: Buffer) => { - // This is horribly memory inefficient but it's only for tests - buffer = Buffer.concat([buffer, data]); + function tryParseEnvelopeFromStdoutLine(line: string): void { + // Lines can have leading '[something] [{' which we need to remove + const cleanedLine = line.replace(/^.*?] \[{"/, '[{"'); - let splitIndex = -1; - while ((splitIndex = buffer.indexOf(0xa)) >= 0) { - const line = buffer.subarray(0, splitIndex).toString(); - buffer = Buffer.from(buffer.subarray(splitIndex + 1)); - // eslint-disable-next-line no-console - if (process.env.DEBUG) console.log('line', line); - tryParseEnvelopeFromStdoutLine(line); + // See if we have a port message + if (cleanedLine.startsWith('{"port":')) { + const { port } = JSON.parse(cleanedLine) as { port: number }; + scenarioServerPort = port; + return; + } + + // Skip any lines that don't start with envelope JSON + if (!cleanedLine.startsWith('[{')) { + return; + } + + try { + const envelope = JSON.parse(cleanedLine) as Envelope; + newEnvelope(envelope); + } catch (_) { + // + } } - }); - }); + + let buffer = Buffer.alloc(0); + child.stdout.on('data', (data: Buffer) => { + // This is horribly memory inefficient but it's only for tests + buffer = Buffer.concat([buffer, data]); + + let splitIndex = -1; + while ((splitIndex = buffer.indexOf(0xa)) >= 0) { + const line = buffer.subarray(0, splitIndex).toString(); + buffer = Buffer.from(buffer.subarray(splitIndex + 1)); + // eslint-disable-next-line no-console + if (process.env.DEBUG) console.log('line', line); + tryParseEnvelopeFromStdoutLine(line); + } + }); + }) + .catch(e => complete(e)); return { childHasExited: function (): boolean { diff --git a/yarn.lock b/yarn.lock index 12799a710b48..ecbf1157aa3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12744,6 +12744,11 @@ denque@^1.4.1: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -16129,6 +16134,13 @@ gcp-metadata@^4.2.0: gaxios "^4.0.0" json-bigint "^1.0.0" +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -18407,6 +18419,11 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + is-reference@1.2.1, is-reference@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -20640,6 +20657,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -20708,6 +20730,16 @@ lru-cache@^7.10.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-cache@^8.0.0: + version "8.0.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e" + integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA== + lru-cache@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83" @@ -22331,6 +22363,20 @@ mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== +mysql2@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.7.1.tgz#bb088fa3f01deefbfe04adaf0d3ec18571b33410" + integrity sha512-4EEqYu57mnkW5+Bvp5wBebY7PpfyrmvJ3knHcmLkp8FyBu4kqgrF2GxIjsC2tbLNZWqJaL21v/MYH7bU5f03oA== + dependencies: + denque "^2.1.0" + generate-function "^2.3.1" + iconv-lite "^0.6.3" + long "^5.2.1" + lru-cache "^8.0.0" + named-placeholders "^1.1.3" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + mysql@^2.18.1: version "2.18.1" resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" @@ -22350,6 +22396,13 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +named-placeholders@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== + dependencies: + lru-cache "^7.14.1" + nan@^2.12.1: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" @@ -27953,6 +28006,11 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -28813,6 +28871,11 @@ sqlstring@2.3.1: resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== + sri-toolbox@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/sri-toolbox/-/sri-toolbox-0.2.0.tgz#a7fea5c3fde55e675cf1c8c06f3ebb5c2935835e" From 256b8be7824e4312a7bbda08d4662979dbdb6f15 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 25 Jan 2024 20:21:44 +0000 Subject: [PATCH 06/39] test(node): Add mutation case to GraphQL OTEL integration tests. (#10352) Adds a mutation scenario to GraphQL OTEL integration tests. --- .../apollo-graphql/scenario-mutation.js | 63 ++++++++++++++ .../{scenario.js => scenario-query.js} | 0 .../apollo-graphql/test.ts | 86 ++++++++++++------- 3 files changed, 118 insertions(+), 31 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js rename dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/{scenario.js => scenario-query.js} (100%) diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js new file mode 100644 index 000000000000..ebe4f7cd3e4d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-mutation.js @@ -0,0 +1,63 @@ +const Sentry = require('@sentry/node-experimental'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +async function run() { + const { ApolloServer, gql } = require('apollo-server'); + + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async span => { + const server = new ApolloServer({ + typeDefs: gql` + type Query { + hello: String + } + type Mutation { + login(email: String): String + } + `, + resolvers: { + Query: { + hello: () => { + return 'Hello world!'; + }, + }, + Mutation: { + login: async (_, { email }) => { + return `${email}--token`; + }, + }, + }, + }); + + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: gql`mutation Mutation($email: String){ + login(email: $email) + }`, + variables: { email: 'test@email.com' }, + }); + + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-query.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario.js rename to dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/scenario-query.js diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts index dc7c304484f9..96018c12ebeb 100644 --- a/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/apollo-graphql/test.ts @@ -2,37 +2,61 @@ import { conditionalTest } from '../../../utils'; import { createRunner } from '../../../utils/runner'; conditionalTest({ min: 14 })('GraphQL/Apollo Tests', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.type': 'query', - 'graphql.source': '{hello}', - 'otel.kind': 'INTERNAL', - 'sentry.origin': 'auto.graphql.otel.graphql', - }, - description: 'query', - status: 'ok', - origin: 'auto.graphql.otel.graphql', - }), - expect.objectContaining({ - data: { - 'graphql.field.name': 'hello', - 'graphql.field.path': 'hello', - 'graphql.field.type': 'String', - 'graphql.source': 'hello', - 'otel.kind': 'INTERNAL', - 'sentry.origin': 'manual', - }, - description: 'graphql.resolve', - status: 'ok', - origin: 'manual', - }), - ]), - }; - test('CJS - should instrument GraphQL queries used from Apollo Server.', done => { - createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: { + 'graphql.operation.type': 'query', + 'graphql.source': '{hello}', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.graphql.otel.graphql', + }, + description: 'query', + status: 'ok', + origin: 'auto.graphql.otel.graphql', + }), + expect.objectContaining({ + data: { + 'graphql.field.name': 'hello', + 'graphql.field.path': 'hello', + 'graphql.field.type': 'String', + 'graphql.source': 'hello', + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'manual', + }, + description: 'graphql.resolve', + status: 'ok', + origin: 'manual', + }), + ]), + }; + + createRunner(__dirname, 'scenario-query.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + }); + + test('CJS - should instrument GraphQL mutations used from Apollo Server.', done => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: { + 'graphql.operation.name': 'Mutation', + 'graphql.operation.type': 'mutation', + 'graphql.source': `mutation Mutation($email: String) { + login(email: $email) +}`, + 'otel.kind': 'INTERNAL', + 'sentry.origin': 'auto.graphql.otel.graphql', + }, + description: 'mutation Mutation', + status: 'ok', + origin: 'auto.graphql.otel.graphql', + }), + ]), + }; + + createRunner(__dirname, 'scenario-mutation.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); }); }); From bd8d8bb724a98fb267194b20d2b4b848aab07068 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 25 Jan 2024 21:23:23 +0100 Subject: [PATCH 07/39] test(node): Add `pg` auto instrumentation test for `@sentry/node-experimental` (#10347) --- .../postgres/docker-compose.yml | 13 +++++ .../tracing-experimental/postgres/scenario.js | 46 ++++++++++++++++ .../tracing-experimental/postgres/test.ts | 54 +++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml new file mode 100644 index 000000000000..dac954ad81d7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.9' + +services: + db: + image: postgres:13 + restart: always + container_name: integration-tests-postgres + ports: + - '5444:5432' + environment: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: tests diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js new file mode 100644 index 000000000000..fa81bd00b938 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/scenario.js @@ -0,0 +1,46 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node-experimental'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const { Client } = require('pg'); + +const client = new Client({ port: 5444, user: 'test', password: 'test', database: 'tests' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await client.connect(); + + await client + .query( + 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));', + ) + .catch(() => { + // if this is not a fresh database, the table might already exist + }); + + await client.query('INSERT INTO "User" ("email", "name") VALUES ($1, $2)', ['tim', 'tim@domain.com']); + await client.query('SELECT * FROM "User"'); + } finally { + await client.end(); + } + }, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts new file mode 100644 index 000000000000..117a5d80ac02 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/postgres/test.ts @@ -0,0 +1,54 @@ +import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; + +conditionalTest({ min: 14 })('postgres auto instrumentation', () => { + test('should auto-instrument `pg` package', done => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'sentry.origin': 'manual', + 'sentry.op': 'db', + }), + description: 'pg.connect', + op: 'db', + status: 'ok', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'INSERT INTO "User" ("email", "name") VALUES ($1, $2)', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'SELECT * FROM "User"', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'SELECT * FROM "User"', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', + }), + ]), + }; + + createRunner(__dirname, 'scenario.js') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start(done); + }); +}); From e0869ab5fe305fc61e476780e465957273483af4 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 25 Jan 2024 15:48:55 -0500 Subject: [PATCH 08/39] fix(deno): Call function if client is not setup (#10354) I think this conditional needs to still call the deno cron `fn()`, and that we should do an early return if the client is **not** on `SETUP_CLIENTS` --- packages/deno/src/integrations/deno-cron.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts index 3b337b004405..a524b927acca 100644 --- a/packages/deno/src/integrations/deno-cron.ts +++ b/packages/deno/src/integrations/deno-cron.ts @@ -37,8 +37,8 @@ const denoCronIntegration = (() => { } async function cronCalled(): Promise { - if (SETUP_CLIENTS.has(getClient() as Client)) { - return; + if (!SETUP_CLIENTS.has(getClient() as Client)) { + return fn(); } await withMonitor(monitorSlug, async () => fn(), { From 2879478a50d147fdd1b69bc266f3eb17c743ec13 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 25 Jan 2024 16:29:45 -0500 Subject: [PATCH 09/39] feat(deno): Expose functional integrations to replace classes (#10355) Refactor deno integrations to functional style. --- MIGRATION.md | 7 +++++-- packages/deno/src/index.ts | 11 +++++++--- packages/deno/src/integrations/context.ts | 14 ++++++++++--- .../deno/src/integrations/contextlines.ts | 21 ++++++++++++------- packages/deno/src/integrations/deno-cron.ts | 14 ++++++++++--- .../deno/src/integrations/globalhandlers.ts | 13 ++++++++++-- packages/deno/src/integrations/index.ts | 1 + .../deno/src/integrations/normalizepaths.ts | 14 ++++++++++--- packages/deno/src/sdk.ts | 13 +++++++----- 9 files changed, 80 insertions(+), 28 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 4c0ea3eddc91..f92022cc4690 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -53,12 +53,15 @@ The following list shows how integrations should be migrated: | `new RewriteFrames()` | `rewriteFramesIntegration()` | `@sentry/integrations` | | `new SessionTiming()` | `sessionTimingIntegration()` | `@sentry/integrations` | | `new HttpClient()` | `httpClientIntegration()` | `@sentry/integrations` | -| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser` | +| `new ContextLines()` | `contextLinesIntegration()` | `@sentry/browser`, `@sentry/deno` | | `new Breadcrumbs()` | `breadcrumbsIntegration()` | `@sentry/browser`, `@sentry/deno` | -| `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` | +| `new GlobalHandlers()` | `globalHandlersIntegration()` | `@sentry/browser` , `@sentry/deno` | | `new HttpContext()` | `httpContextIntegration()` | `@sentry/browser` | | `new TryCatch()` | `browserApiErrorsIntegration()` | `@sentry/browser`, `@sentry/deno` | | `new VueIntegration()` | `vueIntegration()` | `@sentry/vue` | +| `new DenoContext()` | `denoContextIntegration()` | `@sentry/deno` | +| `new DenoCron()` | `denoCronIntegration()` | `@sentry/deno` | +| `new NormalizePaths()` | `normalizePathsIntegration()` | `@sentry/deno` | ## Deprecate `hub.bindClient()` and `makeMain()` diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 65d82a0e6779..a622df3111e6 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -97,12 +97,17 @@ export { export { breadcrumbsIntegration, dedupeIntegration } from '@sentry/browser'; import { Integrations as CoreIntegrations } from '@sentry/core'; +export { denoContextIntegration } from './integrations/context'; +export { globalHandlersIntegration } from './integrations/globalhandlers'; +export { normalizePathsIntegration } from './integrations/normalizepaths'; +export { contextLinesIntegration } from './integrations/contextlines'; +export { denoCronIntegration } from './integrations/deno-cron'; + import * as DenoIntegrations from './integrations'; -const INTEGRATIONS = { +/** @deprecated Import the integration function directly, e.g. `inboundFiltersIntegration()` instead of `new Integrations.InboundFilter(). */ +export const Integrations = { // eslint-disable-next-line deprecation/deprecation ...CoreIntegrations, ...DenoIntegrations, }; - -export { INTEGRATIONS as Integrations }; diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts index 199da80d9b4b..f844b80be6c8 100644 --- a/packages/deno/src/integrations/context.ts +++ b/packages/deno/src/integrations/context.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; const INTEGRATION_NAME = 'DenoContext'; @@ -52,7 +52,7 @@ async function addDenoRuntimeContext(event: Event): Promise { return event; } -const denoContextIntegration = (() => { +const _denoContextIntegration = (() => { return { name: INTEGRATION_NAME, // TODO v8: Remove this @@ -63,8 +63,16 @@ const denoContextIntegration = (() => { }; }) satisfies IntegrationFn; -/** Adds Deno context to events. */ +export const denoContextIntegration = defineIntegration(_denoContextIntegration); + +/** + * Adds Deno context to events. + * @deprecated Use `denoContextintegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const DenoContext = convertIntegrationFnToClass(INTEGRATION_NAME, denoContextIntegration) as IntegrationClass< Integration & { processEvent: (event: Event) => Promise } >; + +// eslint-disable-next-line deprecation/deprecation +export type DenoContext = typeof DenoContext; diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts index 1b3b413699f6..fc51e4ad2d57 100644 --- a/packages/deno/src/integrations/contextlines.ts +++ b/packages/deno/src/integrations/contextlines.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Event, Integration, IntegrationClass, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; @@ -47,7 +47,7 @@ interface ContextLinesOptions { frameContextLines?: number; } -const denoContextLinesIntegration = ((options: ContextLinesOptions = {}) => { +const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; return { @@ -60,12 +60,19 @@ const denoContextLinesIntegration = ((options: ContextLinesOptions = {}) => { }; }) satisfies IntegrationFn; -/** Add node modules / packages to the event */ +export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); + +/** + * Add node modules / packages to the event. + * @deprecated Use `contextLinesIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration) as IntegrationClass< + Integration & { processEvent: (event: Event) => Promise } +>; + // eslint-disable-next-line deprecation/deprecation -export const ContextLines = convertIntegrationFnToClass( - INTEGRATION_NAME, - denoContextLinesIntegration, -) as IntegrationClass Promise }>; +export type ContextLines = typeof ContextLines; /** Processes an event and adds context lines */ async function addSourceContext(event: Event, contextLines: number): Promise { diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts index a524b927acca..89030629864c 100644 --- a/packages/deno/src/integrations/deno-cron.ts +++ b/packages/deno/src/integrations/deno-cron.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass, getClient, withMonitor } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration, getClient, withMonitor } from '@sentry/core'; import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { parseScheduleToString } from './deno-cron-format'; @@ -11,7 +11,7 @@ const INTEGRATION_NAME = 'DenoCron'; const SETUP_CLIENTS = new WeakMap(); -const denoCronIntegration = (() => { +const _denoCronIntegration = (() => { return { name: INTEGRATION_NAME, setupOnce() { @@ -60,8 +60,16 @@ const denoCronIntegration = (() => { }; }) satisfies IntegrationFn; -/** Instruments Deno.cron to automatically capture cron check-ins */ +export const denoCronIntegration = defineIntegration(_denoCronIntegration); + +/** + * Instruments Deno.cron to automatically capture cron check-ins. + * @deprecated Use `denoCronIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const DenoCron = convertIntegrationFnToClass(INTEGRATION_NAME, denoCronIntegration) as IntegrationClass< Integration & { setup: (client: Client) => void } >; + +// eslint-disable-next-line deprecation/deprecation +export type DenoCron = typeof DenoCron; diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index 895c52ee59e4..0c830c40da25 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -1,4 +1,5 @@ import type { ServerRuntimeClient } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; import { convertIntegrationFnToClass } from '@sentry/core'; import { captureEvent } from '@sentry/core'; import { getClient } from '@sentry/core'; @@ -21,7 +22,7 @@ type GlobalHandlersIntegrations = Record { +const _globalHandlersIntegration = ((options?: GlobalHandlersIntegrations) => { const _options = { error: true, unhandledrejection: true, @@ -43,13 +44,21 @@ const globalHandlersIntegration = ((options?: GlobalHandlersIntegrations) => { }; }) satisfies IntegrationFn; -/** Global handlers */ +export const globalHandlersIntegration = defineIntegration(_globalHandlersIntegration); + +/** + * Global handlers. + * @deprecated Use `globalHandlersIntergation()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const GlobalHandlers = convertIntegrationFnToClass( INTEGRATION_NAME, globalHandlersIntegration, ) as IntegrationClass void }>; +// eslint-disable-next-line deprecation/deprecation +export type GlobalHandlers = typeof GlobalHandlers; + function installGlobalErrorHandler(client: Client): void { globalThis.addEventListener('error', data => { if (getClient() !== client || isExiting) { diff --git a/packages/deno/src/integrations/index.ts b/packages/deno/src/integrations/index.ts index 065e16770109..6870606066eb 100644 --- a/packages/deno/src/integrations/index.ts +++ b/packages/deno/src/integrations/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ export { DenoContext } from './context'; export { GlobalHandlers } from './globalhandlers'; export { NormalizePaths } from './normalizepaths'; diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts index 68ba3986e805..a9b8f3dbb0e3 100644 --- a/packages/deno/src/integrations/normalizepaths.ts +++ b/packages/deno/src/integrations/normalizepaths.ts @@ -1,4 +1,4 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { createStackParser, dirname, nodeStackLineParser } from '@sentry/utils'; @@ -55,7 +55,7 @@ function getCwd(): string | undefined { return undefined; } -const normalizePathsIntegration = (() => { +const _normalizePathsIntegration = (() => { // Cached here let appRoot: string | undefined; @@ -98,9 +98,17 @@ const normalizePathsIntegration = (() => { }; }) satisfies IntegrationFn; -/** Normalises paths to the app root directory. */ +export const normalizePathsIntegration = defineIntegration(_normalizePathsIntegration); + +/** + * Normalises paths to the app root directory. + * @deprecated Use `normalizePathsIntegration()` instead. + */ // eslint-disable-next-line deprecation/deprecation export const NormalizePaths = convertIntegrationFnToClass( INTEGRATION_NAME, normalizePathsIntegration, ) as IntegrationClass Event }>; + +// eslint-disable-next-line deprecation/deprecation +export type NormalizePaths = typeof NormalizePaths; diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index 990eb8146039..c5bd3a1d002f 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -6,7 +6,10 @@ import type { Integration, Options, StackParser } from '@sentry/types'; import { createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; import { DenoClient } from './client'; -import { ContextLines, DenoContext, GlobalHandlers, NormalizePaths } from './integrations'; +import { denoContextIntegration } from './integrations/context'; +import { contextLinesIntegration } from './integrations/contextlines'; +import { globalHandlersIntegration } from './integrations/globalhandlers'; +import { normalizePathsIntegration } from './integrations/normalizepaths'; import { makeFetchTransport } from './transports'; import type { DenoOptions } from './types'; @@ -24,10 +27,10 @@ export const defaultIntegrations = [ xhr: false, }), // Deno Specific - new DenoContext(), - new ContextLines(), - new NormalizePaths(), - new GlobalHandlers(), + denoContextIntegration(), + contextLinesIntegration(), + normalizePathsIntegration(), + globalHandlersIntegration(), ]; /** Get the default integrations for the Deno SDK. */ From adccbe6e6c0075c90810e0435209420998f06358 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 26 Jan 2024 10:54:19 -0500 Subject: [PATCH 10/39] feat(tracing): Expose new `browserTracingIntegration` (#10351) Extracted this out of https://github.com/getsentry/sentry-javascript/pull/10327. This PR: * Introduces a new `browserTracingIntegration()` * Does NOT deprecate BrowserTracing yet, as custom implementations in Angular, Next, Sveltekit have not been migrated over yet, which would be weird. We can deprecate it once we moved these over. * Makes sure that custom implementations in Next & Sveltekit are "fixed" automatically * Uses a slim fork for the CDN bundles, to avoid shipping multiple implementations in there. * This means that in the CDN bundles, you can already use the new syntax, but you cannot pass a custom routing instrumentation anymore, and you also don't have the utility functions for it yet. I think this is the best tradeoff for now, and it's probably not a super common case to have custom routing instrumentation when using the Loader/CDN bundles (and if you do, you have to stick to `new BrowserTracing()` until v8). I copied the browser integration tests we have, which all pass! --- .size-limit.js | 7 + .../replay/replayIntegrationShim /init.js | 23 + .../replayIntegrationShim /template.html | 9 + .../replay/replayIntegrationShim /test.ts | 35 ++ .../backgroundtab-custom/init.js | 9 + .../backgroundtab-custom/subject.js | 11 + .../backgroundtab-custom/template.html | 10 + .../backgroundtab-custom/test.ts | 45 ++ .../backgroundtab-pageload/subject.js | 8 + .../backgroundtab-pageload/template.html | 9 + .../backgroundtab-pageload/test.ts | 23 + .../http-timings/init.js | 16 + .../http-timings/subject.js | 1 + .../http-timings/test.ts | 58 ++ .../tracing/browserTracingIntegration/init.js | 9 + .../interactions/assets/script.js | 17 + .../interactions/init.js | 17 + .../interactions/template.html | 12 + .../interactions/test.ts | 114 ++++ .../long-tasks-disabled/assets/script.js | 12 + .../long-tasks-disabled/init.js | 9 + .../long-tasks-disabled/template.html | 10 + .../long-tasks-disabled/test.ts | 23 + .../long-tasks-enabled/assets/script.js | 12 + .../long-tasks-enabled/init.js | 13 + .../long-tasks-enabled/template.html | 10 + .../long-tasks-enabled/test.ts | 38 ++ .../browserTracingIntegration/meta/init.js | 10 + .../meta/template.html | 11 + .../browserTracingIntegration/meta/test.ts | 96 +++ .../navigation/test.ts | 51 ++ .../pageload/init.js | 10 + .../pageload/test.ts | 24 + .../pageloadDelayed/init.js | 13 + .../pageloadDelayed/test.ts | 26 + .../pageloadWithHeartbeatTimeout/init.js | 14 + .../pageloadWithHeartbeatTimeout/test.ts | 27 + .../customTargets/init.js | 9 + .../customTargets/subject.js | 1 + .../customTargets/test.ts | 33 ++ .../customTargetsAndOrigins/init.js | 11 + .../customTargetsAndOrigins/subject.js | 1 + .../customTargetsAndOrigins/test.ts | 32 + .../customTracingOrigins/init.js | 9 + .../customTracingOrigins/subject.js | 1 + .../customTracingOrigins/test.ts | 32 + .../defaultTargetsMatch/init.js | 9 + .../defaultTargetsMatch/subject.js | 1 + .../defaultTargetsMatch/test.ts | 32 + .../defaultTargetsNoMatch/init.js | 9 + .../defaultTargetsNoMatch/subject.js | 1 + .../defaultTargetsNoMatch/test.ts | 32 + .../browserTracingIntegrationHashShim/init.js | 12 + .../template.html | 9 + .../browserTracingIntegrationHashShim/test.ts | 36 ++ .../browserTracingIntegrationShim/init.js | 2 +- .../browserTracingIntegrationShim/test.ts | 2 +- packages/astro/test/client/sdk.test.ts | 21 +- packages/browser/src/helpers.ts | 34 +- packages/browser/src/index.bundle.feedback.ts | 9 +- packages/browser/src/index.bundle.replay.ts | 2 + .../index.bundle.tracing.replay.feedback.ts | 2 + .../src/index.bundle.tracing.replay.ts | 2 + packages/browser/src/index.bundle.tracing.ts | 2 + packages/browser/src/index.bundle.ts | 2 + packages/browser/src/index.ts | 5 + packages/core/src/baseclient.ts | 13 + packages/core/src/tracing/trace.ts | 117 +--- .../integration-shims/src/BrowserTracing.ts | 11 +- packages/integration-shims/src/index.ts | 9 +- packages/nextjs/src/client/index.ts | 23 +- packages/nextjs/test/clientSdk.test.ts | 25 +- packages/sveltekit/src/client/sdk.ts | 21 +- packages/sveltekit/test/client/sdk.test.ts | 23 +- .../src/browser/browserTracingIntegration.ts | 549 ++++++++++++++++++ .../src/browser/browsertracing.ts | 18 +- .../tracing-internal/src/browser/index.ts | 8 + packages/tracing-internal/src/index.ts | 5 + packages/types/src/client.ts | 21 + packages/types/src/index.ts | 1 + packages/types/src/startSpanOptions.ts | 108 ++++ 81 files changed, 2008 insertions(+), 139 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js create mode 100644 dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html create mode 100644 dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts create mode 100644 packages/tracing-internal/src/browser/browserTracingIntegration.ts create mode 100644 packages/types/src/startSpanOptions.ts diff --git a/.size-limit.js b/.size-limit.js index 1a60e556e3e8..5e94a923e656 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -47,6 +47,13 @@ module.exports = [ gzip: true, limit: '35 KB', }, + { + name: '@sentry/browser (incl. browserTracingIntegration) - Webpack (gzipped)', + path: 'packages/browser/build/npm/esm/index.js', + import: '{ init, browserTracingIntegration }', + gzip: true, + limit: '35 KB', + }, { name: '@sentry/browser (incl. Feedback) - Webpack (gzipped)', path: 'packages/browser/build/npm/esm/index.js', diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js new file mode 100644 index 000000000000..9200b5771ec6 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /init.js @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +// Replay should not actually work, but still not error out +window.Replay = new Sentry.replayIntegration({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + integrations: [window.Replay], +}); + +// Ensure none of these break +window.Replay.start(); +window.Replay.stop(); +window.Replay.flush(); diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts new file mode 100644 index 000000000000..6817367ee68d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim /test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; + +sentryTest( + 'exports a shim replayIntegration integration for non-replay bundles', + async ({ getLocalTestPath, page, forceFlushReplay }) => { + const bundle = process.env.PW_BUNDLE; + + if (!bundle || !bundle.startsWith('bundle_') || bundle.includes('replay')) { + sentryTest.skip(); + } + + const consoleMessages: string[] = []; + page.on('console', msg => consoleMessages.push(msg.text())); + + let requestCount = 0; + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + requestCount++; + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await forceFlushReplay(); + + expect(requestCount).toBe(0); + expect(consoleMessages).toEqual(['You are using new Replay() even though this bundle does not include replay.']); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js new file mode 100644 index 000000000000..e5453b648509 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ idleTimeout: 9000 })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js new file mode 100644 index 000000000000..5355521f1655 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/subject.js @@ -0,0 +1,11 @@ +document.getElementById('go-background').addEventListener('click', () => { + Object.defineProperty(document, 'hidden', { value: true, writable: true }); + const ev = document.createEvent('Event'); + ev.initEvent('visibilitychange'); + document.dispatchEvent(ev); +}); + +document.getElementById('start-transaction').addEventListener('click', () => { + window.transaction = Sentry.startTransaction({ name: 'test-transaction' }); + Sentry.getCurrentHub().configureScope(scope => scope.setSpan(window.transaction)); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html new file mode 100644 index 000000000000..fac45ecebfaf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts new file mode 100644 index 000000000000..de1cd552ccab --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-custom/test.ts @@ -0,0 +1,45 @@ +import type { JSHandle } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +async function getPropertyValue(handle: JSHandle, prop: string) { + return (await handle.getProperty(prop))?.jsonValue(); +} + +sentryTest('should finish a custom transaction when the page goes background', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const pageloadTransaction = await getFirstSentryEnvelopeRequest(page, url); + expect(pageloadTransaction).toBeDefined(); + + await page.locator('#start-transaction').click(); + const transactionHandle = await page.evaluateHandle('window.transaction'); + + const id_before = await getPropertyValue(transactionHandle, 'span_id'); + const name_before = await getPropertyValue(transactionHandle, 'name'); + const status_before = await getPropertyValue(transactionHandle, 'status'); + const tags_before = await getPropertyValue(transactionHandle, 'tags'); + + expect(name_before).toBe('test-transaction'); + expect(status_before).toBeUndefined(); + expect(tags_before).toStrictEqual({}); + + await page.locator('#go-background').click(); + + const id_after = await getPropertyValue(transactionHandle, 'span_id'); + const name_after = await getPropertyValue(transactionHandle, 'name'); + const status_after = await getPropertyValue(transactionHandle, 'status'); + const tags_after = await getPropertyValue(transactionHandle, 'tags'); + + expect(id_before).toBe(id_after); + expect(name_after).toBe(name_before); + expect(status_after).toBe('cancelled'); + expect(tags_after).toStrictEqual({ visibilitychange: 'document.hidden' }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js new file mode 100644 index 000000000000..b657f38ac009 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/subject.js @@ -0,0 +1,8 @@ +document.getElementById('go-background').addEventListener('click', () => { + setTimeout(() => { + Object.defineProperty(document, 'hidden', { value: true, writable: true }); + const ev = document.createEvent('Event'); + ev.initEvent('visibilitychange'); + document.dispatchEvent(ev); + }, 250); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html new file mode 100644 index 000000000000..31cfc73ec3c3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts new file mode 100644 index 000000000000..8432245f9c9b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/backgroundtab-pageload/test.ts @@ -0,0 +1,23 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should finish pageload transaction when the page goes background', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await page.locator('#go-background').click(); + + const pageloadTransaction = await getFirstSentryEnvelopeRequest(page); + + expect(pageloadTransaction.contexts?.trace?.op).toBe('pageload'); + expect(pageloadTransaction.contexts?.trace?.status).toBe('cancelled'); + expect(pageloadTransaction.contexts?.trace?.tags).toMatchObject({ + visibilitychange: 'document.hidden', + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js new file mode 100644 index 000000000000..e32d09a13fab --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 1000, + _experiments: { + enableHTTPTimings: true, + }, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js new file mode 100644 index 000000000000..f62499b1e9c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts new file mode 100644 index 000000000000..b6da7522d82c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts @@ -0,0 +1,58 @@ +import { expect } from '@playwright/test'; +import type { SerializedEvent } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should create fetch spans with http timing @firefox', async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + await page.route('http://example.com/*', async route => { + const request = route.request(); + const postData = await request.postDataJSON(); + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(Object.assign({ id: 1 }, postData)), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const envelopes = await getMultipleSentryEnvelopeRequests(page, 2, { url, timeout: 10000 }); + const tracingEvent = envelopes[envelopes.length - 1]; // last envelope contains tracing data on all browsers + + // eslint-disable-next-line deprecation/deprecation + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(3); + + await page.pause(); + requestSpans?.forEach((span, index) => + expect(span).toMatchObject({ + description: `GET http://example.com/${index}`, + parent_span_id: tracingEvent.contexts?.trace?.span_id, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: tracingEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.request.redirect_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'network.protocol.version': expect.any(String), + }), + }), + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js new file mode 100644 index 000000000000..83076460599f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js new file mode 100644 index 000000000000..a37a2c70ad27 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js @@ -0,0 +1,17 @@ +const delay = e => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < 70) { + // + } + + e.target.classList.add('clicked'); +}; + +document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay); +document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js new file mode 100644 index 000000000000..846538e7f3f0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 1000, + enableLongTask: false, + _experiments: { + enableInteractions: true, + }, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html new file mode 100644 index 000000000000..3357fb20a94e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html @@ -0,0 +1,12 @@ + + + + + + +
Rendered Before Long Task
+ + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts new file mode 100644 index 000000000000..131403756251 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -0,0 +1,114 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event, Span, SpanContext, Transaction } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { + getFirstSentryEnvelopeRequest, + getMultipleSentryEnvelopeRequests, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +type TransactionJSON = ReturnType & { + spans: ReturnType[]; + contexts: SpanContext; + platform: string; + type: string; +}; + +const wait = (time: number) => new Promise(res => setTimeout(res, time)); + +sentryTest('should capture interaction transaction. @firefox', async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); + + await page.locator('[data-test-id=interaction-button]').click(); + await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); + + const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelopes).toHaveLength(1); + + const eventData = envelopes[0]; + + expect(eventData.contexts).toMatchObject({ trace: { op: 'ui.action.click' } }); + expect(eventData.platform).toBe('javascript'); + expect(eventData.type).toBe('transaction'); + expect(eventData.spans).toHaveLength(1); + + const interactionSpan = eventData.spans![0]; + expect(interactionSpan.op).toBe('ui.interaction.click'); + expect(interactionSpan.description).toBe('body > button.clicked'); + expect(interactionSpan.timestamp).toBeDefined(); + + const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000; + expect(interactionSpanDuration).toBeGreaterThan(70); + expect(interactionSpanDuration).toBeLessThan(200); +}); + +sentryTest( + 'should create only one transaction per interaction @firefox', + async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); + + for (let i = 0; i < 4; i++) { + await wait(100); + await page.locator('[data-test-id=interaction-button]').click(); + const envelope = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelope[0].spans).toHaveLength(1); + } + }, +); + +sentryTest( + 'should use the component name for a clicked element when it is available', + async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); + + await page.locator('[data-test-id=annotated-button]').click(); + + const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelopes).toHaveLength(1); + const eventData = envelopes[0]; + + expect(eventData.spans).toHaveLength(1); + + const interactionSpan = eventData.spans![0]; + expect(interactionSpan.op).toBe('ui.interaction.click'); + expect(interactionSpan.description).toBe('body > AnnotatedButton'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js new file mode 100644 index 000000000000..9ac3d6fb33d2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/assets/script.js @@ -0,0 +1,12 @@ +(() => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < 101) { + // + } +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js new file mode 100644 index 000000000000..bde12a1304ed --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ enableLongTask: false, idleTimeout: 9000 })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html new file mode 100644 index 000000000000..5c3a14114991 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/template.html @@ -0,0 +1,10 @@ + + + + + + +
Rendered Before Long Task
+ + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts new file mode 100644 index 000000000000..1f7bb54bb36a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/test.ts @@ -0,0 +1,23 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should not capture long task when flag is disabled.', async ({ browserName, getLocalTestPath, page }) => { + // Long tasks only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + // eslint-disable-next-line deprecation/deprecation + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBe(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js new file mode 100644 index 000000000000..5a2aef02028d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/assets/script.js @@ -0,0 +1,12 @@ +(() => { + const startTime = Date.now(); + + function getElasped() { + const time = Date.now(); + return time - startTime; + } + + while (getElasped() < 105) { + // + } +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js new file mode 100644 index 000000000000..ad1d8832b228 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 9000, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html new file mode 100644 index 000000000000..5c3a14114991 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/template.html @@ -0,0 +1,10 @@ + + + + + + +
Rendered Before Long Task
+ + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts new file mode 100644 index 000000000000..32819fd784e0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/test.ts @@ -0,0 +1,38 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, page }) => { + // Long tasks only work on chrome + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + // eslint-disable-next-line deprecation/deprecation + const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); + + expect(uiSpans?.length).toBeGreaterThan(0); + + const [firstUISpan] = uiSpans || []; + expect(firstUISpan).toEqual( + expect.objectContaining({ + op: 'ui.long-task', + description: 'Main UI thread blocked', + parent_span_id: eventData.contexts?.trace?.span_id, + }), + ); + const start = (firstUISpan as Event)['start_timestamp'] ?? 0; + const end = (firstUISpan as Event)['timestamp'] ?? 0; + const duration = end - start; + + expect(duration).toBeGreaterThanOrEqual(0.1); + expect(duration).toBeLessThanOrEqual(0.15); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js new file mode 100644 index 000000000000..d4c7810ef518 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, + environment: 'staging', +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html new file mode 100644 index 000000000000..09984cb0c488 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/template.html @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts new file mode 100644 index 000000000000..ae89fd383cbb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/meta/test.ts @@ -0,0 +1,96 @@ +import { expect } from '@playwright/test'; +import type { Event, EventEnvelopeHeaders } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeHeaderRequestParser, + getFirstSentryEnvelopeRequest, + shouldSkipTracingTest, +} from '../../../../utils/helpers'; + +sentryTest( + 'should create a pageload transaction based on `sentry-trace` ', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.contexts?.trace).toMatchObject({ + op: 'pageload', + parent_span_id: '1121201211212012', + trace_id: '12312012123120121231201212312012', + }); + + expect(eventData.spans?.length).toBeGreaterThan(0); + }, +); + +sentryTest( + 'should pick up `baggage` tag, propagate the content in transaction and not add own data', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); + + expect(envHeader.trace).toBeDefined(); + expect(envHeader.trace).toEqual({ + release: '2.1.12', + sample_rate: '0.3232', + trace_id: '123', + public_key: 'public', + }); + }, +); + +sentryTest( + "should create a navigation that's not influenced by `sentry-trace` ", + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url); + const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + + expect(pageloadRequest.contexts?.trace).toMatchObject({ + op: 'pageload', + parent_span_id: '1121201211212012', + trace_id: '12312012123120121231201212312012', + }); + + expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + expect(navigationRequest.contexts?.trace?.trace_id).toBeDefined(); + expect(navigationRequest.contexts?.trace?.trace_id).not.toBe(pageloadRequest.contexts?.trace?.trace_id); + + const pageloadSpans = pageloadRequest.spans; + const navigationSpans = navigationRequest.spans; + + const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id; + const navigationSpanId = navigationRequest.contexts?.trace?.span_id; + + expect(pageloadSpanId).toBeDefined(); + expect(navigationSpanId).toBeDefined(); + + pageloadSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: pageloadSpanId, + }), + ); + + navigationSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: navigationSpanId, + }), + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts new file mode 100644 index 000000000000..5a46a65a4392 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts @@ -0,0 +1,51 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should create a navigation transaction on page navigation', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const pageloadRequest = await getFirstSentryEnvelopeRequest(page, url); + const navigationRequest = await getFirstSentryEnvelopeRequest(page, `${url}#foo`); + + expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); + expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + + expect(navigationRequest.transaction_info?.source).toEqual('url'); + + const pageloadTraceId = pageloadRequest.contexts?.trace?.trace_id; + const navigationTraceId = navigationRequest.contexts?.trace?.trace_id; + + expect(pageloadTraceId).toBeDefined(); + expect(navigationTraceId).toBeDefined(); + expect(pageloadTraceId).not.toEqual(navigationTraceId); + + const pageloadSpans = pageloadRequest.spans; + const navigationSpans = navigationRequest.spans; + + const pageloadSpanId = pageloadRequest.contexts?.trace?.span_id; + const navigationSpanId = navigationRequest.contexts?.trace?.span_id; + + expect(pageloadSpanId).toBeDefined(); + expect(navigationSpanId).toBeDefined(); + + pageloadSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: pageloadSpanId, + }), + ); + + navigationSpans?.forEach(span => + expect(span).toMatchObject({ + parent_span_id: navigationSpanId, + }), + ); + + expect(pageloadSpanId).not.toEqual(navigationSpanId); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js new file mode 100644 index 000000000000..1f0b64911a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts new file mode 100644 index 000000000000..6a186b63b02a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should create a pageload transaction', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const timeOrigin = await page.evaluate('window._testBaseTimestamp'); + + const { start_timestamp: startTimestamp } = eventData; + + expect(startTimestamp).toBeCloseTo(timeOrigin, 1); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('url'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js new file mode 100644 index 000000000000..2c5a44a7f76d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +setTimeout(() => { + window._testTimeoutTimestamp = (performance.timeOrigin + performance.now()) / 1000; + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, + }); +}, 250); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts new file mode 100644 index 000000000000..882c08d23c5e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadDelayed/test.ts @@ -0,0 +1,26 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should create a pageload transaction when initialized delayed', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const timeOrigin = await page.evaluate('window._testBaseTimestamp'); + const timeoutTimestamp = await page.evaluate('window._testTimeoutTimestamp'); + + const { start_timestamp: startTimestamp } = eventData; + + expect(startTimestamp).toBeCloseTo(timeOrigin, 1); + expect(startTimestamp).toBeLessThan(timeoutTimestamp); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('url'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js new file mode 100644 index 000000000000..8b12fe807d7b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; +import { startSpanManual } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); + +setTimeout(() => { + startSpanManual({ name: 'pageload-child-span' }, () => {}); +}, 200); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts new file mode 100644 index 000000000000..dbb284aecb3b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageloadWithHeartbeatTimeout/test.ts @@ -0,0 +1,27 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +// This tests asserts that the pageload transaction will finish itself after about 15 seconds (3x5s of heartbeats) if it +// has a child span without adding any additional ones or finishing any of them finishing. All of the child spans that +// are still running should have the status "cancelled". +sentryTest( + 'should send a pageload transaction terminated via heartbeat timeout', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect( + // eslint-disable-next-line deprecation/deprecation + eventData.spans?.find(span => span.description === 'pageload-child-span' && span.status === 'cancelled'), + ).toBeDefined(); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js new file mode 100644 index 000000000000..ad48a291386e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ tracePropagationTargets: ['http://example.com'] })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js new file mode 100644 index 000000000000..f62499b1e9c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts new file mode 100644 index 000000000000..fb6e9e540c46 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargets/test.ts @@ -0,0 +1,33 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest( + 'should attach `sentry-trace` and `baggage` header to request matching tracePropagationTargets', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + for (const request of requests) { + const requestHeaders = request.headers(); + + expect(requestHeaders).toMatchObject({ + 'sentry-trace': expect.any(String), + baggage: expect.any(String), + }); + } + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js new file mode 100644 index 000000000000..572b8c69d4dc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ tracePropagationTargets: [], tracingOrigins: ['http://example.com'] }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js new file mode 100644 index 000000000000..f62499b1e9c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts new file mode 100644 index 000000000000..a6cc58ca46ff --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTargetsAndOrigins/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest( + '[pre-v8] should prefer custom tracePropagationTargets over tracingOrigins', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + for (const request of requests) { + const requestHeaders = request.headers(); + expect(requestHeaders).not.toMatchObject({ + 'sentry-trace': expect.any(String), + baggage: expect.any(String), + }); + } + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js new file mode 100644 index 000000000000..45e5237e4c24 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ tracingOrigins: ['http://example.com'] })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js new file mode 100644 index 000000000000..f62499b1e9c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts new file mode 100644 index 000000000000..9f32b7b1ad28 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/customTracingOrigins/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest( + '[pre-v8] should attach `sentry-trace` and `baggage` header to request matching tracingOrigins', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + for (const request of requests) { + const requestHeaders = request.headers(); + expect(requestHeaders).toMatchObject({ + 'sentry-trace': expect.any(String), + baggage: expect.any(String), + }); + } + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js new file mode 100644 index 000000000000..83076460599f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js new file mode 100644 index 000000000000..4e9cf0d01004 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/subject.js @@ -0,0 +1 @@ +fetch('http://localhost:4200/0').then(fetch('http://localhost:4200/1').then(fetch('http://localhost:4200/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts new file mode 100644 index 000000000000..120b36ec88db --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest( + 'should attach `sentry-trace` and `baggage` header to request matching default tracePropagationTargets', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://localhost:4200/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + for (const request of requests) { + const requestHeaders = request.headers(); + expect(requestHeaders).toMatchObject({ + 'sentry-trace': expect.any(String), + baggage: expect.any(String), + }); + } + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js new file mode 100644 index 000000000000..83076460599f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js new file mode 100644 index 000000000000..f62499b1e9c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts new file mode 100644 index 000000000000..116319259101 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest( + 'should not attach `sentry-trace` and `baggage` header to request not matching default tracePropagationTargets', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + for (const request of requests) { + const requestHeaders = request.headers(); + expect(requestHeaders).not.toMatchObject({ + 'sentry-trace': expect.any(String), + baggage: expect.any(String), + }); + } + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js new file mode 100644 index 000000000000..cd05f29615bb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1, + integrations: [new Sentry.Integrations.BrowserTracing()], +}); + +// This should not fail +Sentry.addTracingExtensions(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts new file mode 100644 index 000000000000..e37181ee815b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationHashShim/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; + +sentryTest( + 'exports a shim Integrations.BrowserTracing integration for non-tracing bundles', + async ({ getLocalTestPath, page }) => { + // Skip in tracing tests + if (!shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const consoleMessages: string[] = []; + page.on('console', msg => consoleMessages.push(msg.text())); + + let requestCount = 0; + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + requestCount++; + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + expect(requestCount).toBe(0); + expect(consoleMessages).toEqual([ + 'You are using new BrowserTracing() even though this bundle does not include tracing.', + ]); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js index cd05f29615bb..e8ba5702cff8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/init.js @@ -5,7 +5,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, - integrations: [new Sentry.Integrations.BrowserTracing()], + integrations: [new Sentry.browserTracingIntegration()], }); // This should not fail diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts index e37181ee815b..71510468a513 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts @@ -4,7 +4,7 @@ import { sentryTest } from '../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../utils/helpers'; sentryTest( - 'exports a shim Integrations.BrowserTracing integration for non-tracing bundles', + 'exports a shim browserTracingIntegration() integration for non-tracing bundles', async ({ getLocalTestPath, page }) => { // Skip in tracing tests if (!shouldSkipTracingTest()) { diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 2e10d4210953..3960c25eccd3 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,5 +1,6 @@ import type { BrowserClient } from '@sentry/browser'; -import { getCurrentScope } from '@sentry/browser'; +import { getActiveSpan } from '@sentry/browser'; +import { browserTracingIntegration, getCurrentScope } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; import { BrowserTracing, SDK_VERSION, WINDOW, getClient } from '@sentry/browser'; import { vi } from 'vitest'; @@ -100,7 +101,7 @@ describe('Sentry client SDK', () => { delete globalThis.__SENTRY_TRACING__; }); - it('Overrides the automatically default BrowserTracing instance with a a user-provided instance', () => { + it('Overrides the automatically default BrowserTracing instance with a a user-provided BrowserTracing instance', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })], @@ -118,6 +119,22 @@ describe('Sentry client SDK', () => { // This shows that the user-configured options are still here expect(options.finalTimeout).toEqual(10); }); + + it('Overrides the automatically default BrowserTracing instance with a a user-provided browserTracingIntergation instance', () => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + browserTracingIntegration({ finalTimeout: 10, instrumentNavigation: false, instrumentPageLoad: false }), + ], + enableTracing: true, + }); + + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); + expect(browserTracing).toBeDefined(); + + // no active span means the settings were respected + expect(getActiveSpan()).toBeUndefined(); + }); }); }); }); diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 1bc58b780748..5fff014eaa8d 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -1,5 +1,7 @@ +import type { browserTracingIntegration } from '@sentry-internal/tracing'; +import { BrowserTracing } from '@sentry-internal/tracing'; import { captureException, withScope } from '@sentry/core'; -import type { DsnLike, Mechanism, WrappedFunction } from '@sentry/types'; +import type { DsnLike, Integration, Mechanism, WrappedFunction } from '@sentry/types'; import { GLOBAL_OBJ, addExceptionMechanism, @@ -185,3 +187,33 @@ export interface ReportDialogOptions { /** Callback after reportDialog closed */ onClose?(this: void): void; } + +/** + * This is a slim shim of `browserTracingIntegration` for the CDN bundles. + * Since the actual functional integration uses a different code from `BrowserTracing`, + * we want to avoid shipping both of them in the CDN bundles, as that would blow up the size. + * Instead, we provide a functional integration with the same API, but the old implementation. + * This means that it's not possible to register custom routing instrumentation, but that's OK for now. + * We also don't expose the utilities for this anyhow in the CDN bundles. + * For users that need custom routing in CDN bundles, they have to continue using `new BrowserTracing()` until v8. + */ +export function bundleBrowserTracingIntegration( + options: Parameters[0] = {}, +): Integration { + // Migrate some options from the old integration to the new one + const opts: ConstructorParameters[0] = options; + + if (typeof options.markBackgroundSpan === 'boolean') { + opts.markBackgroundTransactions = options.markBackgroundSpan; + } + + if (typeof options.instrumentPageLoad === 'boolean') { + opts.startTransactionOnPageLoad = options.instrumentPageLoad; + } + + if (typeof options.instrumentNavigation === 'boolean') { + opts.startTransactionOnLocationChange = options.instrumentNavigation; + } + + return new BrowserTracing(opts); +} diff --git a/packages/browser/src/index.bundle.feedback.ts b/packages/browser/src/index.bundle.feedback.ts index 5d3612106286..af4de5ea063d 100644 --- a/packages/browser/src/index.bundle.feedback.ts +++ b/packages/browser/src/index.bundle.feedback.ts @@ -1,6 +1,12 @@ // This is exported so the loader does not fail when switching off Replay/Tracing import { Feedback, feedbackIntegration } from '@sentry-internal/feedback'; -import { BrowserTracing, Replay, addTracingExtensions, replayIntegration } from '@sentry-internal/integration-shims'; +import { + BrowserTracing, + Replay, + addTracingExtensions, + browserTracingIntegration, + replayIntegration, +} from '@sentry-internal/integration-shims'; import * as Sentry from './index.bundle.base'; @@ -13,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing; export * from './index.bundle.base'; export { BrowserTracing, + browserTracingIntegration, addTracingExtensions, // eslint-disable-next-line deprecation/deprecation Replay, diff --git a/packages/browser/src/index.bundle.replay.ts b/packages/browser/src/index.bundle.replay.ts index 2609e7d9b48c..175a435fadcf 100644 --- a/packages/browser/src/index.bundle.replay.ts +++ b/packages/browser/src/index.bundle.replay.ts @@ -3,6 +3,7 @@ import { BrowserTracing, Feedback, addTracingExtensions, + browserTracingIntegration, feedbackIntegration, } from '@sentry-internal/integration-shims'; import { Replay, replayIntegration } from '@sentry/replay'; @@ -18,6 +19,7 @@ Sentry.Integrations.BrowserTracing = BrowserTracing; export * from './index.bundle.base'; export { BrowserTracing, + browserTracingIntegration, addTracingExtensions, // eslint-disable-next-line deprecation/deprecation Replay, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index e17c7de4159a..df151bba0a8f 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -1,6 +1,7 @@ import { Feedback, feedbackIntegration } from '@sentry-internal/feedback'; import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing'; import { Replay, replayIntegration } from '@sentry/replay'; +import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers'; import * as Sentry from './index.bundle.base'; @@ -23,6 +24,7 @@ export { feedbackIntegration, replayIntegration, BrowserTracing, + browserTracingIntegration, Span, addExtensionMethods, }; diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 5dc0537be064..2437a8546d5c 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -1,6 +1,7 @@ import { Feedback, feedbackIntegration } from '@sentry-internal/integration-shims'; import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing'; import { Replay, replayIntegration } from '@sentry/replay'; +import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers'; import * as Sentry from './index.bundle.base'; @@ -23,6 +24,7 @@ export { replayIntegration, feedbackIntegration, BrowserTracing, + browserTracingIntegration, Span, addExtensionMethods, }; diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index f810b61b92a7..2ca0613146f0 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -1,6 +1,7 @@ // This is exported so the loader does not fail when switching off Replay import { Feedback, Replay, feedbackIntegration, replayIntegration } from '@sentry-internal/integration-shims'; import { BrowserTracing, Span, addExtensionMethods } from '@sentry-internal/tracing'; +import { bundleBrowserTracingIntegration as browserTracingIntegration } from './helpers'; import * as Sentry from './index.bundle.base'; @@ -23,6 +24,7 @@ export { feedbackIntegration, replayIntegration, BrowserTracing, + browserTracingIntegration, Span, addExtensionMethods, }; diff --git a/packages/browser/src/index.bundle.ts b/packages/browser/src/index.bundle.ts index a92ff6bf66ec..93a0b0cb498a 100644 --- a/packages/browser/src/index.bundle.ts +++ b/packages/browser/src/index.bundle.ts @@ -4,6 +4,7 @@ import { Feedback, Replay, addTracingExtensions, + browserTracingIntegration, feedbackIntegration, replayIntegration, } from '@sentry-internal/integration-shims'; @@ -24,6 +25,7 @@ export { Replay, // eslint-disable-next-line deprecation/deprecation Feedback, + browserTracingIntegration, feedbackIntegration, replayIntegration, }; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 19c377fc5931..0c75bae6e1f9 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -57,6 +57,11 @@ export { BrowserTracing, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, } from '@sentry-internal/tracing'; export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; export { diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index c7736e278c1c..a4d43fc58a8a 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -26,6 +26,7 @@ import type { SessionAggregates, Severity, SeverityLevel, + StartSpanOptions, Transaction, TransactionEvent, Transport, @@ -482,6 +483,12 @@ export abstract class BaseClient implements Client { callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void, ): void; + /** @inheritdoc */ + public on(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void; + + /** @inheritdoc */ + public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + /** @inheritdoc */ public on(hook: string, callback: unknown): void { if (!this._hooks[hook]) { @@ -522,6 +529,12 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public emit(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay: boolean }): void; + /** @inheritdoc */ + public emit(hook: 'startPageLoadSpan', options: StartSpanOptions): void; + + /** @inheritdoc */ + public emit(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /** @inheritdoc */ public emit(hook: string, ...rest: unknown[]): void { if (this._hooks[hook]) { diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index bb92373d3e58..885cbd7c9d08 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,15 +1,5 @@ -import type { - Instrumenter, - Primitive, - Scope, - Span, - SpanTimeInput, - TransactionContext, - TransactionMetadata, -} from '@sentry/types'; -import type { SpanAttributes } from '@sentry/types'; -import type { SpanOrigin } from '@sentry/types'; -import type { TransactionSource } from '@sentry/types'; +import type { Span, SpanTimeInput, StartSpanOptions, TransactionContext } from '@sentry/types'; + import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -20,109 +10,6 @@ import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import { spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; -interface StartSpanOptions extends Omit { - /** A manually specified start time for the created `Span` object. */ - startTime?: SpanTimeInput; - - /** If defined, start this span off this scope instead off the current scope. */ - scope?: Scope; - - /** The name of the span. */ - name: string; - - /** An op for the span. This is a categorization for spans. */ - op?: string; - - /** - * The origin of the span - if it comes from auto instrumentation or manual instrumentation. - * - * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead. - */ - origin?: SpanOrigin; - - /** Attributes for the span. */ - attributes?: SpanAttributes; - - // All remaining fields are deprecated - - /** - * @deprecated Manually set the end timestamp instead. - */ - trimEnd?: boolean; - - /** - * @deprecated This cannot be set manually anymore. - */ - parentSampled?: boolean; - - /** - * @deprecated Use attributes or set data on scopes instead. - */ - metadata?: Partial; - - /** - * The name thingy. - * @deprecated Use `name` instead. - */ - description?: string; - - /** - * @deprecated Use `span.setStatus()` instead. - */ - status?: string; - - /** - * @deprecated Use `scope` instead. - */ - parentSpanId?: string; - - /** - * @deprecated You cannot manually set the span to sampled anymore. - */ - sampled?: boolean; - - /** - * @deprecated You cannot manually set the spanId anymore. - */ - spanId?: string; - - /** - * @deprecated You cannot manually set the traceId anymore. - */ - traceId?: string; - - /** - * @deprecated Use an attribute instead. - */ - source?: TransactionSource; - - /** - * @deprecated Use attributes or set tags on the scope instead. - */ - tags?: { [key: string]: Primitive }; - - /** - * @deprecated Use attributes instead. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: { [key: string]: any }; - - /** - * @deprecated Use `startTime` instead. - */ - startTimestamp?: number; - - /** - * @deprecated Use `span.end()` instead. - */ - endTimestamp?: number; - - /** - * @deprecated You cannot set the instrumenter manually anymore. - */ - instrumenter?: Instrumenter; -} - /** * Wraps a function with a transaction/span and finishes the span after the function is done. * diff --git a/packages/integration-shims/src/BrowserTracing.ts b/packages/integration-shims/src/BrowserTracing.ts index 310dc589afe9..8e3d61bae58f 100644 --- a/packages/integration-shims/src/BrowserTracing.ts +++ b/packages/integration-shims/src/BrowserTracing.ts @@ -33,7 +33,16 @@ class BrowserTracingShim implements Integration { } } -export { BrowserTracingShim as BrowserTracing }; +/** + * This is a shim for the BrowserTracing integration. + * It is needed in order for the CDN bundles to continue working when users add/remove tracing + * from it, without changing their config. This is necessary for the loader mechanism. + */ +function browserTracingIntegrationShim(_options: unknown): Integration { + return new BrowserTracingShim({}); +} + +export { BrowserTracingShim as BrowserTracing, browserTracingIntegrationShim as browserTracingIntegration }; /** Shim function */ export function addTracingExtensions(): void { diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts index 43243f69a194..bffdf82c99f7 100644 --- a/packages/integration-shims/src/index.ts +++ b/packages/integration-shims/src/index.ts @@ -3,9 +3,16 @@ export { Feedback, feedbackIntegration, } from './Feedback'; + export { // eslint-disable-next-line deprecation/deprecation Replay, replayIntegration, } from './Replay'; -export { BrowserTracing, addTracingExtensions } from './BrowserTracing'; + +export { + // eslint-disable-next-line deprecation/deprecation + BrowserTracing, + browserTracingIntegration, + addTracingExtensions, +} from './BrowserTracing'; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index d1d5e1db7ff5..a1c20937f578 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,5 +1,5 @@ import { applySdkMetadata, hasTracingEnabled } from '@sentry/core'; -import type { BrowserOptions } from '@sentry/react'; +import type { BrowserOptions, browserTracingIntegration } from '@sentry/react'; import { Integrations as OriginalIntegrations, getCurrentScope, @@ -86,13 +86,30 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void { } } +function isNewBrowserTracingIntegration( + integration: Integration, +): integration is Integration & { options?: Parameters[0] } { + return !!integration.afterAllSetup && !!(integration as BrowserTracing).options; +} + function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] { const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing'); + + if (!browserTracing) { + return integrations; + } + + // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one + if (isNewBrowserTracingIntegration(browserTracing)) { + const { options } = browserTracing; + integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); + } + // If BrowserTracing was added, but it is not our forked version, // replace it with our forked version with the same options - if (browserTracing && !(browserTracing instanceof BrowserTracing)) { + if (!(browserTracing instanceof BrowserTracing)) { const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options; - // These two options are overwritten by the custom integration + // This option is overwritten by the custom integration delete options.routingInstrumentation; // eslint-disable-next-line deprecation/deprecation delete options.tracingOrigins; diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index 464b7db14dc7..f4ec99c3cc71 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,6 +1,7 @@ import { BaseClient } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import type { BrowserClient } from '@sentry/react'; +import { browserTracingIntegration } from '@sentry/react'; import { WINDOW, getClient, getCurrentScope } from '@sentry/react'; import type { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -166,7 +167,7 @@ describe('Client init()', () => { init({ dsn: TEST_DSN, tracesSampleRate: 1.0, - integrations: [new BrowserTracing({ startTransactionOnLocationChange: false })], + integrations: [new BrowserTracing({ finalTimeout: 10 })], }); const client = getClient()!; @@ -177,7 +178,27 @@ describe('Client init()', () => { expect.objectContaining({ routingInstrumentation: nextRouterInstrumentation, // This proves it's still the user's copy - startTransactionOnLocationChange: false, + finalTimeout: 10, + }), + ); + }); + + it('forces correct router instrumentation if user provides `browserTracingIntegration`', () => { + init({ + dsn: TEST_DSN, + integrations: [browserTracingIntegration({ finalTimeout: 10 })], + enableTracing: true, + }); + + const client = getClient()!; + const integration = client.getIntegrationByName('BrowserTracing'); + + expect(integration).toBeDefined(); + expect(integration?.options).toEqual( + expect.objectContaining({ + routingInstrumentation: nextRouterInstrumentation, + // This proves it's still the user's copy + finalTimeout: 10, }), ); }); diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts index 7b9c608a862d..920b2db75193 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -1,5 +1,5 @@ import { applySdkMetadata, hasTracingEnabled } from '@sentry/core'; -import type { BrowserOptions } from '@sentry/svelte'; +import type { BrowserOptions, browserTracingIntegration } from '@sentry/svelte'; import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/svelte'; import { WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte'; import type { Integration } from '@sentry/types'; @@ -61,11 +61,28 @@ function fixBrowserTracingIntegration(options: BrowserOptions): void { } } +function isNewBrowserTracingIntegration( + integration: Integration, +): integration is Integration & { options?: Parameters[0] } { + return !!integration.afterAllSetup && !!(integration as BrowserTracing).options; +} + function maybeUpdateBrowserTracingIntegration(integrations: Integration[]): Integration[] { const browserTracing = integrations.find(integration => integration.name === 'BrowserTracing'); + + if (!browserTracing) { + return integrations; + } + + // If `browserTracingIntegration()` was added, we need to force-convert it to our custom one + if (isNewBrowserTracingIntegration(browserTracing)) { + const { options } = browserTracing; + integrations[integrations.indexOf(browserTracing)] = new BrowserTracing(options); + } + // If BrowserTracing was added, but it is not our forked version, // replace it with our forked version with the same options - if (browserTracing && !(browserTracing instanceof BrowserTracing)) { + if (!(browserTracing instanceof BrowserTracing)) { const options: ConstructorParameters[0] = (browserTracing as BrowserTracing).options; // This option is overwritten by the custom integration delete options.routingInstrumentation; diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts index 10292658bc54..4b0afb85bcd8 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -1,7 +1,7 @@ import { getClient, getCurrentScope } from '@sentry/core'; import type { BrowserClient } from '@sentry/svelte'; import * as SentrySvelte from '@sentry/svelte'; -import { SDK_VERSION, WINDOW } from '@sentry/svelte'; +import { SDK_VERSION, WINDOW, browserTracingIntegration } from '@sentry/svelte'; import { vi } from 'vitest'; import { BrowserTracing, init } from '../../src/client'; @@ -100,7 +100,26 @@ describe('Sentry client SDK', () => { it('Merges a user-provided BrowserTracing integration with the automatically added one', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })], + integrations: [new BrowserTracing({ finalTimeout: 10 })], + enableTracing: true, + }); + + const browserTracing = getClient()?.getIntegrationByName('BrowserTracing') as BrowserTracing; + const options = browserTracing.options; + + expect(browserTracing).toBeDefined(); + + // This shows that the user-configured options are still here + expect(options.finalTimeout).toEqual(10); + + // But we force the routing instrumentation to be ours + expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation); + }); + + it('Merges a user-provided browserTracingIntegration with the automatically added one', () => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [browserTracingIntegration({ finalTimeout: 10 })], enableTracing: true, }); diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts new file mode 100644 index 000000000000..8184ad058039 --- /dev/null +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -0,0 +1,549 @@ +/* eslint-disable max-lines, complexity */ +import type { IdleTransaction } from '@sentry/core'; +import { getClient } from '@sentry/core'; +import { defineIntegration, getCurrentHub } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + TRACING_DEFAULTS, + addTracingExtensions, + getActiveTransaction, + spanIsSampled, + spanToJSON, + startIdleTransaction, +} from '@sentry/core'; +import type { + IntegrationFn, + StartSpanOptions, + Transaction, + TransactionContext, + TransactionSource, +} from '@sentry/types'; +import type { Span } from '@sentry/types'; +import { + addHistoryInstrumentationHandler, + browserPerformanceTimeOrigin, + getDomElement, + logger, + tracingContextFromHeaders, +} from '@sentry/utils'; + +import { DEBUG_BUILD } from '../common/debug-build'; +import { registerBackgroundTabDetection } from './backgroundtab'; +import { + addPerformanceEntries, + startTrackingInteractions, + startTrackingLongTasks, + startTrackingWebVitals, +} from './metrics'; +import type { RequestInstrumentationOptions } from './request'; +import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; +import { WINDOW } from './types'; + +export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; + +/** Options for Browser Tracing integration */ +export interface BrowserTracingOptions extends RequestInstrumentationOptions { + /** + * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined + * by a moment where there are no in-progress spans. + * + * The transaction will use the end timestamp of the last finished span as the endtime for the transaction. + * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset. + * Time is in ms. + * + * Default: 1000 + */ + idleTimeout: number; + + /** + * The max duration for a transaction. If a transaction duration hits the `finalTimeout` value, it + * will be finished. + * Time is in ms. + * + * Default: 30000 + */ + finalTimeout: number; + + /** + * The heartbeat interval. If no new spans are started or open spans are finished within 3 heartbeats, + * the transaction will be finished. + * Time is in ms. + * + * Default: 5000 + */ + heartbeatInterval: number; + + /** + * If a span should be created on page load. + * Default: true + */ + instrumentPageLoad: boolean; + + /** + * If a span should be created on navigation (history change). + * Default: true + */ + instrumentNavigation: boolean; + + /** + * Flag spans where tabs moved to background with "cancelled". Browser background tab timing is + * not suited towards doing precise measurements of operations. By default, we recommend that this option + * be enabled as background transactions can mess up your statistics in nondeterministic ways. + * + * Default: true + */ + markBackgroundSpan: boolean; + + /** + * If true, Sentry will capture long tasks and add them to the corresponding transaction. + * + * Default: true + */ + enableLongTask: boolean; + + /** + * _metricOptions allows the user to send options to change how metrics are collected. + * + * _metricOptions is currently experimental. + * + * Default: undefined + */ + _metricOptions?: Partial<{ + /** + * @deprecated This property no longer has any effect and will be removed in v8. + */ + _reportAllChanges: boolean; + }>; + + /** + * _experiments allows the user to send options to define how this integration works. + * Note that the `enableLongTask` options is deprecated in favor of the option at the top level, and will be removed in v8. + * + * TODO (v8): Remove enableLongTask + * + * Default: undefined + */ + _experiments: Partial<{ + enableInteractions: boolean; + }>; + + /** + * A callback which is called before a span for a pageload or navigation is started. + * It receives the options passed to `startSpan`, and expects to return an updated options object. + */ + beforeStartSpan?: (options: StartSpanOptions) => StartSpanOptions; +} + +const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { + ...TRACING_DEFAULTS, + instrumentNavigation: true, + instrumentPageLoad: true, + markBackgroundSpan: true, + enableLongTask: true, + _experiments: {}, + ...defaultRequestInstrumentationOptions, +}; + +let shouldUseDefaultPageLoadSpan = true; +let shouldUseDefaultNavigationSpan = true; + +/** + * The Browser Tracing integration automatically instruments browser pageload/navigation + * actions as transactions, and captures requests, metrics and errors as spans. + * + * The integration can be configured with a variety of options, and can be extended to use + * any routing library. This integration uses {@see IdleTransaction} to create transactions. + */ +export const _browserTracingIntegration = ((_options: Partial = {}) => { + const _hasSetTracePropagationTargets = DEBUG_BUILD + ? !!( + // eslint-disable-next-line deprecation/deprecation + (_options.tracePropagationTargets || _options.tracingOrigins) + ) + : false; + + addTracingExtensions(); + + // TODO (v8): remove this block after tracingOrigins is removed + // Set tracePropagationTargets to tracingOrigins if specified by the user + // In case both are specified, tracePropagationTargets takes precedence + // eslint-disable-next-line deprecation/deprecation + if (!_options.tracePropagationTargets && _options.tracingOrigins) { + // eslint-disable-next-line deprecation/deprecation + _options.tracePropagationTargets = _options.tracingOrigins; + } + + const options = { + ...DEFAULT_BROWSER_TRACING_OPTIONS, + ..._options, + }; + + const _collectWebVitals = startTrackingWebVitals(); + + if (options.enableLongTask) { + startTrackingLongTasks(); + } + if (options._experiments.enableInteractions) { + startTrackingInteractions(); + } + + let latestRouteName: string | undefined; + let latestRouteSource: TransactionSource | undefined; + + /** Create routing idle transaction. */ + function _createRouteTransaction(context: TransactionContext): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation + const hub = getCurrentHub(); + + const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = options; + + const isPageloadTransaction = context.op === 'pageload'; + + const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : ''; + const baggage = isPageloadTransaction ? getMetaContent('baggage') : ''; + const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( + sentryTrace, + baggage, + ); + + const expandedContext: TransactionContext = { + ...context, + ...traceparentData, + metadata: { + // eslint-disable-next-line deprecation/deprecation + ...context.metadata, + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }, + trimEnd: true, + }; + + const finalContext = beforeStartSpan ? beforeStartSpan(expandedContext) : expandedContext; + + // If `beforeStartSpan` set a custom name, record that fact + // eslint-disable-next-line deprecation/deprecation + finalContext.metadata = + finalContext.name !== expandedContext.name + ? // eslint-disable-next-line deprecation/deprecation + { ...finalContext.metadata, source: 'custom' } + : // eslint-disable-next-line deprecation/deprecation + finalContext.metadata; + + latestRouteName = finalContext.name; + latestRouteSource = getSource(finalContext); + + // eslint-disable-next-line deprecation/deprecation + if (finalContext.sampled === false) { + DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); + } + + DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`); + + const { location } = WINDOW; + + const idleTransaction = startIdleTransaction( + hub, + finalContext, + idleTimeout, + finalTimeout, + true, + { location }, // for use in the tracesSampler + heartbeatInterval, + isPageloadTransaction, // should wait for finish signal if it's a pageload transaction + ); + + if (isPageloadTransaction) { + WINDOW.document.addEventListener('readystatechange', () => { + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + }); + + if (['interactive', 'complete'].includes(WINDOW.document.readyState)) { + idleTransaction.sendAutoFinishSignal(); + } + } + + // eslint-disable-next-line deprecation/deprecation + const scope = hub.getScope(); + + // If it's a pageload and there is a meta tag set + // use the traceparentData as the propagation context + if (isPageloadTransaction && traceparentData) { + scope.setPropagationContext(propagationContext); + } else { + // Navigation transactions should set a new propagation context based on the + // created idle transaction. + scope.setPropagationContext({ + traceId: idleTransaction.spanContext().traceId, + spanId: idleTransaction.spanContext().spanId, + parentSpanId: spanToJSON(idleTransaction).parent_span_id, + sampled: spanIsSampled(idleTransaction), + }); + } + + idleTransaction.registerBeforeFinishCallback(transaction => { + _collectWebVitals(); + addPerformanceEntries(transaction); + }); + + return idleTransaction as Transaction; + } + + return { + name: BROWSER_TRACING_INTEGRATION_ID, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setupOnce: () => {}, + afterAllSetup(client) { + const clientOptions = client.getOptions(); + + const { markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments } = + options; + + const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets; + // There are three ways to configure tracePropagationTargets: + // 1. via top level client option `tracePropagationTargets` + // 2. via BrowserTracing option `tracePropagationTargets` + // 3. via BrowserTracing option `tracingOrigins` (deprecated) + // + // To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to + // BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated). + // This is done as it minimizes bundle size (we don't have to have undefined checks). + // + // If both 1 and either one of 2 or 3 are set (from above), we log out a warning. + // eslint-disable-next-line deprecation/deprecation + const tracePropagationTargets = clientOptionsTracePropagationTargets || options.tracePropagationTargets; + if (DEBUG_BUILD && _hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) { + logger.warn( + '[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.', + ); + } + + let activeSpan: Span | undefined; + let startingUrl: string | undefined = WINDOW.location.href; + + if (client.on) { + client.on('startNavigationSpan', (context: StartSpanOptions) => { + // We check this inside of the hook handler, so that if a custom instrumentation triggers this, + // we don't need to check this option in the instrumentation, but can simply invoke it + // without needing to know the options of this integration + if (!options.instrumentNavigation) { + return; + } + + if (activeSpan) { + DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); + // If there's an open transaction on the scope, we need to finish it before creating an new one. + activeSpan.end(); + } + activeSpan = _createRouteTransaction(context); + }); + + client.on('startPageLoadSpan', (context: StartSpanOptions) => { + // We check this inside of the hook handler, so that if a custom instrumentation triggers this, + // we don't need to check this option in the instrumentation, but can simply invoke it + // without needing to know the options of this integration + if (!options.instrumentPageLoad) { + return; + } + + if (activeSpan) { + DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); + // If there's an open transaction on the scope, we need to finish it before creating an new one. + activeSpan.end(); + } + activeSpan = _createRouteTransaction(context); + }); + } + + if (options.instrumentPageLoad && client.emit && shouldUseDefaultPageLoadSpan) { + const context: StartSpanOptions = { + name: WINDOW.location.pathname, + // pageload should always start at timeOrigin (and needs to be in s, not ms) + startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined, + op: 'pageload', + origin: 'auto.pageload.browser', + metadata: { source: 'url' }, + }; + startBrowserTracingPageLoadSpan(context); + } + + if (options.instrumentNavigation && client.emit) { + addHistoryInstrumentationHandler(({ to, from }) => { + /** + * This early return is there to account for some cases where a navigation transaction starts right after + * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't + * create an uneccessary navigation transaction. + * + * This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also + * only be caused in certain development environments where the usage of a hot module reloader is causing + * errors. + */ + if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) { + startingUrl = undefined; + return; + } + + if (from !== to) { + startingUrl = undefined; + // We check this in here again, as a custom instrumentation may have been triggered in the meanwhile + if (shouldUseDefaultNavigationSpan) { + const context: StartSpanOptions = { + name: WINDOW.location.pathname, + op: 'navigation', + origin: 'auto.navigation.browser', + metadata: { source: 'url' }, + }; + + startBrowserTracingNavigationSpan(context); + } + } + }); + } + + if (markBackgroundSpan) { + registerBackgroundTabDetection(); + } + + if (_experiments.enableInteractions) { + registerInteractionListener(options, latestRouteName, latestRouteSource); + } + + instrumentOutgoingRequests({ + traceFetch, + traceXHR, + tracePropagationTargets, + shouldCreateSpanForRequest, + enableHTTPTimings, + }); + }, + // TODO v8: Remove this again + // This is private API that we use to fix converted BrowserTracing integrations in Next.js & SvelteKit + options, + }; +}) satisfies IntegrationFn; + +export const browserTracingIntegration = defineIntegration(_browserTracingIntegration); + +/** + * Manually start a page load span. + * This will only do something if the BrowserTracing integration has been setup. + */ +export function startBrowserTracingPageLoadSpan(spanOptions: StartSpanOptions): void { + const client = getClient(); + if (!client || !client.emit) { + return; + } + + client.emit('startPageLoadSpan', spanOptions); + shouldUseDefaultPageLoadSpan = false; +} + +/** + * Manually start a navigation span. + * This will only do something if the BrowserTracing integration has been setup. + */ +export function startBrowserTracingNavigationSpan(spanOptions: StartSpanOptions): void { + const client = getClient(); + if (!client || !client.emit) { + return; + } + + client.emit('startNavigationSpan', spanOptions); + shouldUseDefaultNavigationSpan = false; +} + +/** + * Use this method if you want to disable the default navigation span. + * This is useful if you want to add custom routing instrumentation. + */ +export function disableDefaultBrowserTracingNavigationSpan(disable = true): void { + shouldUseDefaultNavigationSpan = !disable; +} + +/** + * Use this method if you want to disable the default page load span. + * This is useful if you want to add custom routing instrumentation. + */ +export function disableDefaultBrowserTracingPageLoadSpan(disable = true): void { + shouldUseDefaultPageLoadSpan = !disable; +} + +/** Returns the value of a meta tag */ +export function getMetaContent(metaName: string): string | undefined { + // Can't specify generic to `getDomElement` because tracing can be used + // in a variety of environments, have to disable `no-unsafe-member-access` + // as a result. + const metaTag = getDomElement(`meta[name=${metaName}]`); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return metaTag ? metaTag.getAttribute('content') : undefined; +} + +/** Start listener for interaction transactions */ +function registerInteractionListener( + options: BrowserTracingOptions, + latestRouteName: string | undefined, + latestRouteSource: TransactionSource | undefined, +): void { + let inflightInteractionTransaction: IdleTransaction | undefined; + const registerInteractionTransaction = (): void => { + const { idleTimeout, finalTimeout, heartbeatInterval } = options; + const op = 'ui.action.click'; + + // eslint-disable-next-line deprecation/deprecation + const currentTransaction = getActiveTransaction(); + if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) { + DEBUG_BUILD && + logger.warn( + `[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`, + ); + return undefined; + } + + if (inflightInteractionTransaction) { + inflightInteractionTransaction.setFinishReason('interactionInterrupted'); + inflightInteractionTransaction.end(); + inflightInteractionTransaction = undefined; + } + + if (!latestRouteName) { + DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`); + return undefined; + } + + const { location } = WINDOW; + + const context: TransactionContext = { + name: latestRouteName, + op, + trimEnd: true, + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url', + }, + }; + + inflightInteractionTransaction = startIdleTransaction( + // eslint-disable-next-line deprecation/deprecation + getCurrentHub(), + context, + idleTimeout, + finalTimeout, + true, + { location }, // for use in the tracesSampler + heartbeatInterval, + ); + }; + + ['click'].forEach(type => { + addEventListener(type, registerInteractionTransaction, { once: false, capture: true }); + }); +} + +function getSource(context: TransactionContext): TransactionSource | undefined { + const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = context.metadata && context.metadata.source; + + return sourceFromAttributes || sourceFromData || sourceFromMetadata; +} diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index e9f61c73c0f3..2d8ffd9af135 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -344,13 +344,7 @@ export class BrowserTracing implements Integration { finalContext.metadata; this._latestRouteName = finalContext.name; - - // eslint-disable-next-line deprecation/deprecation - const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; - // eslint-disable-next-line deprecation/deprecation - const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source; - - this._latestRouteSource = sourceFromData || sourceFromMetadata; + this._latestRouteSource = getSource(finalContext); // eslint-disable-next-line deprecation/deprecation if (finalContext.sampled === false) { @@ -481,3 +475,13 @@ export function getMetaContent(metaName: string): string | undefined { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return metaTag ? metaTag.getAttribute('content') : undefined; } + +function getSource(context: TransactionContext): TransactionSource | undefined { + const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = context.metadata && context.metadata.source; + + return sourceFromAttributes || sourceFromData || sourceFromMetadata; +} diff --git a/packages/tracing-internal/src/browser/index.ts b/packages/tracing-internal/src/browser/index.ts index 5b30bc519404..d9b0c347bb4e 100644 --- a/packages/tracing-internal/src/browser/index.ts +++ b/packages/tracing-internal/src/browser/index.ts @@ -3,6 +3,14 @@ export * from '../exports'; export type { RequestInstrumentationOptions } from './request'; export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browsertracing'; +export { + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, +} from './browserTracingIntegration'; + export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './request'; export { diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts index 495d8dbb26b9..d3ace7e56b77 100644 --- a/packages/tracing-internal/src/index.ts +++ b/packages/tracing-internal/src/index.ts @@ -14,6 +14,11 @@ export type { LazyLoadedIntegration } from './node'; export { BrowserTracing, + browserTracingIntegration, + startBrowserTracingNavigationSpan, + startBrowserTracingPageLoadSpan, + disableDefaultBrowserTracingNavigationSpan, + disableDefaultBrowserTracingPageLoadSpan, BROWSER_TRACING_INTEGRATION_ID, instrumentOutgoingRequests, defaultRequestInstrumentationOptions, diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index d8d09ec1431b..5db008b0ba37 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -15,6 +15,7 @@ import type { Scope } from './scope'; import type { SdkMetadata } from './sdkmetadata'; import type { Session, SessionAggregates } from './session'; import type { Severity, SeverityLevel } from './severity'; +import type { StartSpanOptions } from './startSpanOptions'; import type { Transaction } from './transaction'; import type { Transport, TransportMakeRequestResponse } from './transport'; @@ -272,6 +273,16 @@ export interface Client { callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void, ): void; + /** + * A hook for BrowserTracing to trigger a span start for a page load. + */ + on?(hook: 'startPageLoadSpan', callback: (options: StartSpanOptions) => void): void; + + /** + * A hook for BrowserTracing to trigger a span for a navigation. + */ + on?(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void; + /** * Fire a hook event for transaction start. * Expects to be given a transaction as the second argument. @@ -333,5 +344,15 @@ export interface Client { */ emit?(hook: 'beforeSendFeedback', feedback: FeedbackEvent, options?: { includeReplay?: boolean }): void; + /** + * Emit a hook event for BrowserTracing to trigger a span start for a page load. + */ + emit?(hook: 'startPageLoadSpan', options: StartSpanOptions): void; + + /** + * Emit a hook event for BrowserTracing to trigger a span for a navigation. + */ + emit?(hook: 'startNavigationSpan', options: StartSpanOptions): void; + /* eslint-enable @typescript-eslint/unified-signatures */ } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 7f9d66c904fa..5970383febc3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -104,6 +104,7 @@ export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { TextEncoderInternal } from './textencoder'; export type { PropagationContext, TracePropagationTargets } from './tracing'; +export type { StartSpanOptions } from './startSpanOptions'; export type { CustomSamplingContext, SamplingContext, diff --git a/packages/types/src/startSpanOptions.ts b/packages/types/src/startSpanOptions.ts new file mode 100644 index 000000000000..bde20c2c87bf --- /dev/null +++ b/packages/types/src/startSpanOptions.ts @@ -0,0 +1,108 @@ +import type { Instrumenter } from './instrumenter'; +import type { Primitive } from './misc'; +import type { Scope } from './scope'; +import type { SpanAttributes, SpanOrigin, SpanTimeInput } from './span'; +import type { TransactionContext, TransactionMetadata, TransactionSource } from './transaction'; + +export interface StartSpanOptions extends TransactionContext { + /** A manually specified start time for the created `Span` object. */ + startTime?: SpanTimeInput; + + /** If defined, start this span off this scope instead off the current scope. */ + scope?: Scope; + + /** The name of the span. */ + name: string; + + /** An op for the span. This is a categorization for spans. */ + op?: string; + + /** + * The origin of the span - if it comes from auto instrumentation or manual instrumentation. + * + * @deprecated Set `attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]` instead. + */ + origin?: SpanOrigin; + + /** Attributes for the span. */ + attributes?: SpanAttributes; + + // All remaining fields are deprecated + + /** + * @deprecated Manually set the end timestamp instead. + */ + trimEnd?: boolean; + + /** + * @deprecated This cannot be set manually anymore. + */ + parentSampled?: boolean; + + /** + * @deprecated Use attributes or set data on scopes instead. + */ + metadata?: Partial; + + /** + * The name thingy. + * @deprecated Use `name` instead. + */ + description?: string; + + /** + * @deprecated Use `span.setStatus()` instead. + */ + status?: string; + + /** + * @deprecated Use `scope` instead. + */ + parentSpanId?: string; + + /** + * @deprecated You cannot manually set the span to sampled anymore. + */ + sampled?: boolean; + + /** + * @deprecated You cannot manually set the spanId anymore. + */ + spanId?: string; + + /** + * @deprecated You cannot manually set the traceId anymore. + */ + traceId?: string; + + /** + * @deprecated Use an attribute instead. + */ + source?: TransactionSource; + + /** + * @deprecated Use attributes or set tags on the scope instead. + */ + tags?: { [key: string]: Primitive }; + + /** + * @deprecated Use attributes instead. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: { [key: string]: any }; + + /** + * @deprecated Use `startTime` instead. + */ + startTimestamp?: number; + + /** + * @deprecated Use `span.end()` instead. + */ + endTimestamp?: number; + + /** + * @deprecated You cannot set the instrumenter manually anymore. + */ + instrumenter?: Instrumenter; +} From 6b3f553cd2b7074b233d071d962a0e9be41080a8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 26 Jan 2024 13:15:51 -0500 Subject: [PATCH 11/39] fix(core): Make `FunctionToString` integration use SETUP_CLIENTS weakmap (#10358) Auditing our integrations to make sure they work with the new client paradigm, also adding `export type` where appropriate. --- .../core/src/integrations/functiontostring.ts | 27 +++++++++++- .../lib/integrations/functiontostring.test.ts | 42 ++++++++++++++++--- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index 7a50aec67f2d..0f3e9f08b59e 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -1,11 +1,14 @@ -import type { Integration, IntegrationClass, IntegrationFn, WrappedFunction } from '@sentry/types'; +import type { Client, Integration, IntegrationClass, IntegrationFn, WrappedFunction } from '@sentry/types'; import { getOriginalFunction } from '@sentry/utils'; +import { getClient } from '../exports'; import { convertIntegrationFnToClass, defineIntegration } from '../integration'; let originalFunctionToString: () => void; const INTEGRATION_NAME = 'FunctionToString'; +const SETUP_CLIENTS = new WeakMap(); + const _functionToStringIntegration = (() => { return { name: INTEGRATION_NAME, @@ -18,20 +21,37 @@ const _functionToStringIntegration = (() => { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any Function.prototype.toString = function (this: WrappedFunction, ...args: any[]): string { - const context = getOriginalFunction(this) || this; + const originalFunction = getOriginalFunction(this); + const context = + SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined ? originalFunction : this; return originalFunctionToString.apply(context, args); }; } catch { // ignore errors here, just don't patch this } }, + setup(client) { + SETUP_CLIENTS.set(client, true); + }, }; }) satisfies IntegrationFn; +/** + * Patch toString calls to return proper name for wrapped functions. + * + * ```js + * Sentry.init({ + * integrations: [ + * functionToStringIntegration(), + * ], + * }); + * ``` + */ export const functionToStringIntegration = defineIntegration(_functionToStringIntegration); /** * Patch toString calls to return proper name for wrapped functions. + * * @deprecated Use `functionToStringIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation @@ -39,3 +59,6 @@ export const FunctionToString = convertIntegrationFnToClass( INTEGRATION_NAME, functionToStringIntegration, ) as IntegrationClass void }>; + +// eslint-disable-next-line deprecation/deprecation +export type FunctionToString = typeof FunctionToString; diff --git a/packages/core/test/lib/integrations/functiontostring.test.ts b/packages/core/test/lib/integrations/functiontostring.test.ts index bb3b62d11915..c0e2a22cd6ed 100644 --- a/packages/core/test/lib/integrations/functiontostring.test.ts +++ b/packages/core/test/lib/integrations/functiontostring.test.ts @@ -1,7 +1,18 @@ -import { fill } from '../../../../utils/src/object'; -import { FunctionToString } from '../../../src/integrations/functiontostring'; +import { fill } from '@sentry/utils'; +import { getClient, getCurrentScope, setCurrentClient } from '../../../src'; +import { functionToStringIntegration } from '../../../src/integrations/functiontostring'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; describe('FunctionToString', () => { + beforeEach(() => { + const testClient = new TestClient(getDefaultTestClientOptions({})); + setCurrentClient(testClient); + }); + + afterAll(() => { + getCurrentScope().setClient(undefined); + }); + it('it works as expected', () => { const foo = { bar(wat: boolean): boolean { @@ -17,10 +28,31 @@ describe('FunctionToString', () => { expect(foo.bar.toString()).not.toBe(originalFunction); - // eslint-disable-next-line deprecation/deprecation - const fts = new FunctionToString(); - fts.setupOnce(); + const fts = functionToStringIntegration(); + getClient()?.addIntegration?.(fts); expect(foo.bar.toString()).toBe(originalFunction); }); + + it('does not activate when client is not active', () => { + const foo = { + bar(wat: boolean): boolean { + return wat; + }, + }; + const originalFunction = foo.bar.toString(); + fill(foo, 'bar', function wat(whatever: boolean): () => void { + return function watwat(): boolean { + return whatever; + }; + }); + + expect(foo.bar.toString()).not.toBe(originalFunction); + + const testClient = new TestClient(getDefaultTestClientOptions({})); + const fts = functionToStringIntegration(); + testClient.addIntegration(fts); + + expect(foo.bar.toString()).not.toBe(originalFunction); + }); }); From 427fa2093a18cb6bad01a7a3ef879b5f3acb1a1b Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 26 Jan 2024 19:18:04 +0100 Subject: [PATCH 12/39] test(e2e): Add request instrumentation tests for Next.js 14 (#10367) --- .../app/request-instrumentation/page.tsx | 13 ++++++++ .../tests/request-instrumentation.test.ts | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx new file mode 100644 index 000000000000..7a226868d1bd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx @@ -0,0 +1,13 @@ +import http from 'http'; + +export const dynamic = 'force-dynamic'; + +export default async function Page() { + await fetch('http://example.com/'); + await new Promise(resolve => { + http.get('http://example.com/', () => { + resolve(); + }); + }); + return

Hello World!

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts new file mode 100644 index 000000000000..ce17f725cf79 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; + +test('Should send a transaction with a fetch span', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/request-instrumentation)'; + }); + + await page.goto(`/request-instrumentation`); + + expect((await transactionPromise).spans).toContainEqual( + expect.objectContaining({ + data: expect.objectContaining({ + 'http.method': 'GET', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.node.undici', + }), + description: 'GET http://example.com/', + }), + ); + + expect((await transactionPromise).spans).toContainEqual( + expect.objectContaining({ + data: expect.objectContaining({ + 'http.method': 'GET', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.node.http', + }), + description: 'GET http://example.com/', + }), + ); +}); From 3cc7057c52c1421a7d5b4d7346f2e7efaaf0811c Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 26 Jan 2024 19:19:00 +0100 Subject: [PATCH 13/39] ref(browser-integration-tests): Fix flake in interaction transaction tests (#10365) This PR decreases the lower bound of the interaction span duration. In the script file, we hard block for 70ms but I've seen test fails where we're just ever so slightly below 70ms (69.x). Figured this is because of some timing inaccuracies. closes #10363 --- .../tracing/browsertracing/interactions/test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts index 131403756251..fa9d2889bae3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts @@ -1,6 +1,6 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event, Span, SpanContext, Transaction } from '@sentry/types'; +import type { SerializedEvent, Span, SpanContext, Transaction } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { @@ -30,7 +30,7 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + await getFirstSentryEnvelopeRequest(page); await page.locator('[data-test-id=interaction-button]').click(); await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); @@ -51,7 +51,7 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN expect(interactionSpan.timestamp).toBeDefined(); const interactionSpanDuration = (interactionSpan.timestamp! - interactionSpan.start_timestamp) * 1000; - expect(interactionSpanDuration).toBeGreaterThan(70); + expect(interactionSpanDuration).toBeGreaterThan(65); expect(interactionSpanDuration).toBeLessThan(200); }); @@ -70,12 +70,12 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + await getFirstSentryEnvelopeRequest(page); for (let i = 0; i < 4; i++) { await wait(100); await page.locator('[data-test-id=interaction-button]').click(); - const envelope = await getMultipleSentryEnvelopeRequests(page, 1); + const envelope = await getMultipleSentryEnvelopeRequests(page, 1); expect(envelope[0].spans).toHaveLength(1); } }, @@ -97,11 +97,11 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await getFirstSentryEnvelopeRequest(page); + await getFirstSentryEnvelopeRequest(page); await page.locator('[data-test-id=annotated-button]').click(); - const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); expect(envelopes).toHaveLength(1); const eventData = envelopes[0]; From dd77951bd4f89aaedb0e92778b188ba58de5044a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 26 Jan 2024 14:28:46 -0500 Subject: [PATCH 14/39] fix(spotlight): Use unpatched http.request (#10369) To avoid capturing our own requests. See https://github.com/getsentry/spotlight/pull/335 --- packages/node/src/integrations/spotlight.ts | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index f6e5c1e45adc..ab27f860c97b 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -71,7 +71,8 @@ function connectToSpotlight(client: Client, options: Required Date: Fri, 26 Jan 2024 14:52:29 -0500 Subject: [PATCH 15/39] fix: Ensure `afterAllSetup` is called when using `addIntegration()` (#10372) Noticed that this is not really correct right now! --- packages/core/src/baseclient.ts | 7 +++ packages/core/test/lib/integration.test.ts | 57 ++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index a4d43fc58a8a..c64c5cf92c0d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -368,7 +368,14 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public addIntegration(integration: Integration): void { + const isAlreadyInstalled = this._integrations[integration.name]; + + // This hook takes care of only installing if not already installed setupIntegration(this, integration, this._integrations); + // Here we need to check manually to make sure to not run this multiple times + if (!isAlreadyInstalled) { + afterSetupIntegrations(this, [integration]); + } } /** diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index ccc6476c14d8..e819f9413aec 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -646,6 +646,63 @@ describe('addIntegration', () => { expect(warnings).toHaveBeenCalledTimes(1); expect(warnings).toHaveBeenCalledWith('Cannot add integration "test" because no SDK Client is available.'); }); + + it('triggers all hooks', () => { + const setup = jest.fn(); + const setupOnce = jest.fn(); + const setupAfterAll = jest.fn(); + + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = setupOnce; + setup = setup; + afterAllSetup = setupAfterAll; + } + + const client = getTestClient(); + const hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + + const integration = new CustomIntegration(); + addIntegration(integration); + + expect(setupOnce).toHaveBeenCalledTimes(1); + expect(setup).toHaveBeenCalledTimes(1); + expect(setupAfterAll).toHaveBeenCalledTimes(1); + }); + + it('does not trigger hooks if already installed', () => { + const logs = jest.spyOn(logger, 'log'); + + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + setup = jest.fn(); + afterAllSetup = jest.fn(); + } + + const client = getTestClient(); + const hub = new Hub(client); + // eslint-disable-next-line deprecation/deprecation + makeMain(hub); + + const integration1 = new CustomIntegration(); + const integration2 = new CustomIntegration(); + addIntegration(integration1); + + expect(integration1.setupOnce).toHaveBeenCalledTimes(1); + expect(integration1.setup).toHaveBeenCalledTimes(1); + expect(integration1.afterAllSetup).toHaveBeenCalledTimes(1); + + addIntegration(integration2); + + expect(integration2.setupOnce).toHaveBeenCalledTimes(0); + expect(integration2.setup).toHaveBeenCalledTimes(0); + expect(integration2.afterAllSetup).toHaveBeenCalledTimes(0); + + expect(logs).toHaveBeenCalledWith('Integration skipped because it was already installed: test'); + }); }); describe('convertIntegrationFnToClass', () => { From 71a84abad13556d630d0e51459ac4ab9e24cb333 Mon Sep 17 00:00:00 2001 From: Philipp Kammerer <85898598+kammeph@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:09:44 +0100 Subject: [PATCH 16/39] fix(core): Export `spanToTraceContext` function from span utils (#10364) Before submitting a pull request, please take a look at our [Contributing](https://github.com/getsentry/sentry-javascript/blob/master/CONTRIBUTING.md) guidelines and verify: - [x] Ensure your code lints and the test suite passes (`yarn lint`) & (`yarn test`). Closes #10362 --- packages/core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 261a40a1fa8f..849e34f6c92b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -87,6 +87,7 @@ export { spanToTraceHeader, spanToJSON, spanIsSampled, + spanToTraceContext, } from './utils/spanUtils'; export { getRootSpan } from './utils/getRootSpan'; export { applySdkMetadata } from './utils/sdkMetadata'; From 1f3a796e904e2f84148db80304cb5bdb83a04cb1 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 26 Jan 2024 15:33:14 -0500 Subject: [PATCH 17/39] docs: Add docs on building the SDK (#10368) As this comes up every now and then (e.g. https://github.com/getsentry/sentry-javascript/issues/5108), I gather it makes sense to document how to build the SDK youself. I added a note to explicitly state that there are no guarantees there. --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 148045151aac..9ab617cdeffe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,11 @@ able to use it. From the top level of the repo, there are three commands availab dependencies (`utils`, `core`, `browser`, etc), and all packages which depend on it (currently `gatsby` and `nextjs`)) - `yarn build:dev:watch`, which runs `yarn build:dev` in watch mode (recommended) +You can also run a production build via `yarn build`, which will build everything except for the tarballs for publishing to NPM. +You can use this if you want to bundle Sentry yourself. The build output can be found in the packages `build/` folder, e.g. `packages/browser/build`. +Bundled files can be found in `packages/browser/build/bundles`. +Note that there are no guarantees about the produced file names etc., so make sure to double check which files are generated after upgrading. + ## Testing SDK Packages Locally To test local versions of SDK packages, for instance in test projects, you have a couple of options: From 0612090e026dd95c55a68d405e846305413f55ff Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 29 Jan 2024 10:16:13 +0100 Subject: [PATCH 18/39] feat(remix): Export missing functions (#10385) Looks like we missed re-exporting a couple of recently added functions in the Remix SDK. This patch adds the missing exports: * `addIntegration` * `getClient` * `getCurrentScope`, `getIsolationScope`, `getGlobalScope` * `setMeasurement` * `getActiveSpan` * `startSpan`, `startSpanManual`, `startInactiveSpan` * `continueTrace` * `isInitialized` --- CONTRIBUTING.md | 9 +++++---- packages/remix/src/index.server.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ab617cdeffe..812aaa870df5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,10 +37,11 @@ able to use it. From the top level of the repo, there are three commands availab dependencies (`utils`, `core`, `browser`, etc), and all packages which depend on it (currently `gatsby` and `nextjs`)) - `yarn build:dev:watch`, which runs `yarn build:dev` in watch mode (recommended) -You can also run a production build via `yarn build`, which will build everything except for the tarballs for publishing to NPM. -You can use this if you want to bundle Sentry yourself. The build output can be found in the packages `build/` folder, e.g. `packages/browser/build`. -Bundled files can be found in `packages/browser/build/bundles`. -Note that there are no guarantees about the produced file names etc., so make sure to double check which files are generated after upgrading. +You can also run a production build via `yarn build`, which will build everything except for the tarballs for publishing +to NPM. You can use this if you want to bundle Sentry yourself. The build output can be found in the packages `build/` +folder, e.g. `packages/browser/build`. Bundled files can be found in `packages/browser/build/bundles`. Note that there +are no guarantees about the produced file names etc., so make sure to double check which files are generated after +upgrading. ## Testing SDK Packages Locally diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 1cd04720b4dc..62879c11520f 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -15,6 +15,7 @@ export { addGlobalEventProcessor, addEventProcessor, addBreadcrumb, + addIntegration, captureCheckIn, withMonitor, captureException, @@ -30,6 +31,10 @@ export { getHubFromCarrier, // eslint-disable-next-line deprecation/deprecation getCurrentHub, + getClient, + getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, // eslint-disable-next-line deprecation/deprecation makeMain, @@ -67,6 +72,13 @@ export { deepReadDirSync, Integrations, Handlers, + setMeasurement, + getActiveSpan, + startSpan, + startSpanManual, + startInactiveSpan, + continueTrace, + isInitialized, cron, parameterize, } from '@sentry/node'; From 8554dcab6b74e25e78931ec564a0a0dfa36254a2 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 29 Jan 2024 10:16:58 +0100 Subject: [PATCH 19/39] feat(core): Deprecate `Span.setHttpStatus` in favor of `setHttpStatus` (#10268) Deprecate the last remaining otel-incompatible API on the `Span` class and interface - `Span.setHttpStatus`. We replace this functionality with a `setHttpStatus` wrapper because in addition to setting the data/tag value (more on that below), this function also derives the span status from the status code. Added tests to make sure this is covered properly (I think it wasn't covered well before and we need to remove the old tests anyway). --- CHANGELOG.md | 2 +- biome.json | 1 + packages/astro/src/server/middleware.ts | 4 +- packages/bun/src/integrations/bunserver.ts | 6 +- packages/core/src/tracing/errors.ts | 2 +- packages/core/src/tracing/index.ts | 5 +- packages/core/src/tracing/span.ts | 94 +-------------- packages/core/src/tracing/spanstatus.ts | 107 ++++++++++++++++++ .../core/test/lib/tracing/spanstatus.test.ts | 41 +++++++ .../src/common/utils/edgeWrapperUtils.ts | 11 +- .../nextjs/src/common/utils/responseEnd.ts | 4 +- .../src/common/wrapApiHandlerWithSentry.ts | 13 ++- .../src/common/wrapRouteHandlerWithSentry.ts | 3 +- packages/node/src/handlers.ts | 3 +- packages/node/src/integrations/hapi/index.ts | 9 +- packages/node/src/integrations/http.ts | 5 +- .../node/src/integrations/undici/index.ts | 3 +- packages/remix/src/utils/instrumentServer.ts | 3 +- .../remix/src/utils/serverAdapters/express.ts | 11 +- packages/serverless/src/gcpfunction/http.ts | 7 +- packages/serverless/test/gcpfunction.test.ts | 6 +- packages/sveltekit/src/server/handle.ts | 3 +- .../tracing-internal/src/browser/request.ts | 3 +- packages/tracing-internal/src/common/fetch.ts | 3 +- packages/tracing/test/span.test.ts | 1 + packages/types/src/span.ts | 1 + 26 files changed, 225 insertions(+), 126 deletions(-) create mode 100644 packages/core/test/lib/tracing/spanstatus.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ddfdfb5b45..23b13973847a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1103,7 +1103,7 @@ finished. This is useful for event emitters or similar. function middleware(_req, res, next) { return Sentry.startSpanManual({ name: 'middleware' }, (span, finish) => { res.once('finish', () => { - span?.setHttpStatus(res.status); + setHttpStatus(span, res.status); finish(); }); return next(); diff --git a/biome.json b/biome.json index a795c92a22df..ff5a6ac17286 100644 --- a/biome.json +++ b/biome.json @@ -47,6 +47,7 @@ "dev-packages/browser-integration-tests/suites/**/*.json", "dev-packages/browser-integration-tests/loader-suites/**/*.js", "dev-packages/browser-integration-tests/suites/stacktraces/**/*.js", + ".next/**/*", "**/fixtures/*/*.json", "**/*.min.js", ".next/**", diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index a66c942076b1..8a7cc3d90384 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus } from '@sentry/core'; import { captureException, continueTrace, @@ -142,7 +142,7 @@ async function instrumentRequest( const originalResponse = await next(); if (span && originalResponse.status) { - span.setHttpStatus(originalResponse.status); + setHttpStatus(span, originalResponse.status); } const scope = getCurrentScope(); diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index aa8765638647..b1dc4c6892e0 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -7,6 +7,7 @@ import { convertIntegrationFnToClass, getCurrentScope, runWithAsyncContext, + setHttpStatus, startSpan, } from '@sentry/core'; import type { IntegrationFn } from '@sentry/types'; @@ -93,8 +94,9 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] typeof serveOptions.fetch >); if (response && response.status) { - span?.setHttpStatus(response.status); - span?.setAttribute('http.response.status_code', response.status); + if (span) { + setHttpStatus(span, response.status); + } if (span instanceof Transaction) { const scope = getCurrentScope(); scope.setContext('response', { diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts index 5a885dd1f090..229695afc58c 100644 --- a/packages/core/src/tracing/errors.ts +++ b/packages/core/src/tracing/errors.ts @@ -5,7 +5,7 @@ import { } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import type { SpanStatusType } from './span'; +import type { SpanStatusType } from './spanstatus'; import { getActiveTransaction } from './utils'; let errorsInstrumented = false; diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index ecdc5f595095..948196dd13c2 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -1,13 +1,14 @@ export { startIdleTransaction, addTracingExtensions } from './hubextensions'; export { IdleTransaction, TRACING_DEFAULTS } from './idletransaction'; export type { BeforeFinishCallback } from './idletransaction'; -export { Span, spanStatusfromHttpCode } from './span'; +export { Span } from './span'; export { Transaction } from './transaction'; // eslint-disable-next-line deprecation/deprecation export { extractTraceparentData, getActiveTransaction } from './utils'; // eslint-disable-next-line deprecation/deprecation export { SpanStatus } from './spanstatus'; -export type { SpanStatusType } from './span'; +export { setHttpStatus, spanStatusfromHttpCode } from './spanstatus'; +export type { SpanStatusType } from './spanstatus'; export { // eslint-disable-next-line deprecation/deprecation trace, diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index a52d6bd5e9c8..165677455d7f 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -26,6 +26,8 @@ import { spanToTraceContext, spanToTraceHeader, } from '../utils/spanUtils'; +import type { SpanStatusType } from './spanstatus'; +import { setHttpStatus } from './spanstatus'; /** * Keeps track of finished spans for a given transaction @@ -470,16 +472,10 @@ export class Span implements SpanInterface { /** * @inheritDoc + * @deprecated Use top-level `setHttpStatus()` instead. */ public setHttpStatus(httpStatus: number): this { - // eslint-disable-next-line deprecation/deprecation - this.setTag('http.status_code', String(httpStatus)); - // eslint-disable-next-line deprecation/deprecation - this.setData('http.response.status_code', httpStatus); - const spanStatus = spanStatusfromHttpCode(httpStatus); - if (spanStatus !== 'unknown_error') { - this.setStatus(spanStatus); - } + setHttpStatus(this, httpStatus); return this; } @@ -675,85 +671,3 @@ export class Span implements SpanInterface { return hasData ? data : attributes; } } - -export type SpanStatusType = - /** The operation completed successfully. */ - | 'ok' - /** Deadline expired before operation could complete. */ - | 'deadline_exceeded' - /** 401 Unauthorized (actually does mean unauthenticated according to RFC 7235) */ - | 'unauthenticated' - /** 403 Forbidden */ - | 'permission_denied' - /** 404 Not Found. Some requested entity (file or directory) was not found. */ - | 'not_found' - /** 429 Too Many Requests */ - | 'resource_exhausted' - /** Client specified an invalid argument. 4xx. */ - | 'invalid_argument' - /** 501 Not Implemented */ - | 'unimplemented' - /** 503 Service Unavailable */ - | 'unavailable' - /** Other/generic 5xx. */ - | 'internal_error' - /** Unknown. Any non-standard HTTP status code. */ - | 'unknown_error' - /** The operation was cancelled (typically by the user). */ - | 'cancelled' - /** Already exists (409) */ - | 'already_exists' - /** Operation was rejected because the system is not in a state required for the operation's */ - | 'failed_precondition' - /** The operation was aborted, typically due to a concurrency issue. */ - | 'aborted' - /** Operation was attempted past the valid range. */ - | 'out_of_range' - /** Unrecoverable data loss or corruption */ - | 'data_loss'; - -/** - * Converts a HTTP status code into a {@link SpanStatusType}. - * - * @param httpStatus The HTTP response status code. - * @returns The span status or unknown_error. - */ -export function spanStatusfromHttpCode(httpStatus: number): SpanStatusType { - if (httpStatus < 400 && httpStatus >= 100) { - return 'ok'; - } - - if (httpStatus >= 400 && httpStatus < 500) { - switch (httpStatus) { - case 401: - return 'unauthenticated'; - case 403: - return 'permission_denied'; - case 404: - return 'not_found'; - case 409: - return 'already_exists'; - case 413: - return 'failed_precondition'; - case 429: - return 'resource_exhausted'; - default: - return 'invalid_argument'; - } - } - - if (httpStatus >= 500 && httpStatus < 600) { - switch (httpStatus) { - case 501: - return 'unimplemented'; - case 503: - return 'unavailable'; - case 504: - return 'deadline_exceeded'; - default: - return 'internal_error'; - } - } - - return 'unknown_error'; -} diff --git a/packages/core/src/tracing/spanstatus.ts b/packages/core/src/tracing/spanstatus.ts index 6a758d95ee84..f38c397f1ae0 100644 --- a/packages/core/src/tracing/spanstatus.ts +++ b/packages/core/src/tracing/spanstatus.ts @@ -1,3 +1,5 @@ +import type { Span } from '@sentry/types'; + /** The status of an Span. * * @deprecated Use string literals - if you require type casting, cast to SpanStatusType type @@ -38,3 +40,108 @@ export enum SpanStatus { /** Unrecoverable data loss or corruption */ DataLoss = 'data_loss', } + +export type SpanStatusType = + /** The operation completed successfully. */ + | 'ok' + /** Deadline expired before operation could complete. */ + | 'deadline_exceeded' + /** 401 Unauthorized (actually does mean unauthenticated according to RFC 7235) */ + | 'unauthenticated' + /** 403 Forbidden */ + | 'permission_denied' + /** 404 Not Found. Some requested entity (file or directory) was not found. */ + | 'not_found' + /** 429 Too Many Requests */ + | 'resource_exhausted' + /** Client specified an invalid argument. 4xx. */ + | 'invalid_argument' + /** 501 Not Implemented */ + | 'unimplemented' + /** 503 Service Unavailable */ + | 'unavailable' + /** Other/generic 5xx. */ + | 'internal_error' + /** Unknown. Any non-standard HTTP status code. */ + | 'unknown_error' + /** The operation was cancelled (typically by the user). */ + | 'cancelled' + /** Already exists (409) */ + | 'already_exists' + /** Operation was rejected because the system is not in a state required for the operation's */ + | 'failed_precondition' + /** The operation was aborted, typically due to a concurrency issue. */ + | 'aborted' + /** Operation was attempted past the valid range. */ + | 'out_of_range' + /** Unrecoverable data loss or corruption */ + | 'data_loss'; + +/** + * Converts a HTTP status code into a {@link SpanStatusType}. + * + * @param httpStatus The HTTP response status code. + * @returns The span status or unknown_error. + */ +export function spanStatusfromHttpCode(httpStatus: number): SpanStatusType { + if (httpStatus < 400 && httpStatus >= 100) { + return 'ok'; + } + + if (httpStatus >= 400 && httpStatus < 500) { + switch (httpStatus) { + case 401: + return 'unauthenticated'; + case 403: + return 'permission_denied'; + case 404: + return 'not_found'; + case 409: + return 'already_exists'; + case 413: + return 'failed_precondition'; + case 429: + return 'resource_exhausted'; + default: + return 'invalid_argument'; + } + } + + if (httpStatus >= 500 && httpStatus < 600) { + switch (httpStatus) { + case 501: + return 'unimplemented'; + case 503: + return 'unavailable'; + case 504: + return 'deadline_exceeded'; + default: + return 'internal_error'; + } + } + + return 'unknown_error'; +} + +/** + * Sets the Http status attributes on the current span based on the http code. + * Additionally, the span's status is updated, depending on the http code. + */ +export function setHttpStatus(span: Span, httpStatus: number): void { + // TODO (v8): Remove these calls + // Relay does not require us to send the status code as a tag + // For now, just because users might expect it to land as a tag we keep sending it. + // Same with data. + // In v8, we replace both, simply with + // span.setAttribute('http.response.status_code', httpStatus); + + // eslint-disable-next-line deprecation/deprecation + span.setTag('http.status_code', String(httpStatus)); + // eslint-disable-next-line deprecation/deprecation + span.setData('http.response.status_code', httpStatus); + + const spanStatus = spanStatusfromHttpCode(httpStatus); + if (spanStatus !== 'unknown_error') { + span.setStatus(spanStatus); + } +} diff --git a/packages/core/test/lib/tracing/spanstatus.test.ts b/packages/core/test/lib/tracing/spanstatus.test.ts new file mode 100644 index 000000000000..559aa101c642 --- /dev/null +++ b/packages/core/test/lib/tracing/spanstatus.test.ts @@ -0,0 +1,41 @@ +import { Span, setHttpStatus, spanToJSON } from '../../../src/index'; + +describe('setHttpStatus', () => { + it.each([ + [200, 'ok'], + [300, 'ok'], + [401, 'unauthenticated'], + [403, 'permission_denied'], + [404, 'not_found'], + [409, 'already_exists'], + [413, 'failed_precondition'], + [429, 'resource_exhausted'], + [455, 'invalid_argument'], + [501, 'unimplemented'], + [503, 'unavailable'], + [504, 'deadline_exceeded'], + [520, 'internal_error'], + ])('applies the correct span status and http status code to the span (%s - $%s)', (code, status) => { + const span = new Span({ name: 'test' }); + + setHttpStatus(span!, code); + + const { status: spanStatus, data, tags } = spanToJSON(span!); + + expect(spanStatus).toBe(status); + expect(data).toMatchObject({ 'http.response.status_code': code }); + expect(tags).toMatchObject({ 'http.status_code': String(code) }); + }); + + it("doesn't set the status for an unknown http status code", () => { + const span = new Span({ name: 'test' }); + + setHttpStatus(span!, 600); + + const { status: spanStatus, data, tags } = spanToJSON(span!); + + expect(spanStatus).toBeUndefined(); + expect(data).toMatchObject({ 'http.response.status_code': 600 }); + expect(tags).toMatchObject({ 'http.status_code': '600' }); + }); +}); diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 109a586d7cd6..fd98fe2328ee 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -5,6 +5,7 @@ import { captureException, continueTrace, handleCallbackErrors, + setHttpStatus, startSpan, } from '@sentry/core'; import { winterCGRequestToRequestData } from '@sentry/utils'; @@ -67,10 +68,12 @@ export function withEdgeWrapping( }, ); - if (handlerResult instanceof Response) { - span?.setHttpStatus(handlerResult.status); - } else { - span?.setStatus('ok'); + if (span) { + if (handlerResult instanceof Response) { + setHttpStatus(span, handlerResult.status); + } else { + span.setStatus('ok'); + } } return handlerResult; diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index e59a99fb0ebb..c12d19f1c6fa 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -1,5 +1,5 @@ import type { ServerResponse } from 'http'; -import { flush } from '@sentry/core'; +import { flush, setHttpStatus } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { fill, logger } from '@sentry/utils'; @@ -41,7 +41,7 @@ export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: S /** Finish the given response's transaction and set HTTP status data */ export function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): void { if (transaction) { - transaction.setHttpStatus(res.statusCode); + setHttpStatus(transaction, res.statusCode); transaction.end(); } } diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index 91c95e264875..61a08ba18891 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -5,6 +5,7 @@ import { continueTrace, getCurrentScope, runWithAsyncContext, + setHttpStatus, startSpanManual, } from '@sentry/core'; import { consoleSandbox, isString, logger, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; @@ -123,8 +124,10 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri // eslint-disable-next-line @typescript-eslint/unbound-method res.end = new Proxy(res.end, { apply(target, thisArg, argArray) { - span?.setHttpStatus(res.statusCode); - span?.end(); + if (span) { + setHttpStatus(span, res.statusCode); + span.end(); + } if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { target.apply(thisArg, argArray); } else { @@ -180,8 +183,10 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri res.statusCode = 500; res.statusMessage = 'Internal Server Error'; - span?.setHttpStatus(res.statusCode); - span?.end(); + if (span) { + setHttpStatus(span, res.statusCode); + span.end(); + } // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index a1067b1577e4..99b9cf99b9b9 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -4,6 +4,7 @@ import { getCurrentScope, handleCallbackErrors, runWithAsyncContext, + setHttpStatus, startSpan, } from '@sentry/core'; import { tracingContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; @@ -66,7 +67,7 @@ export function wrapRouteHandlerWithSentry any>( ); try { - span?.setHttpStatus(response.status); + span && setHttpStatus(span, response.status); } catch { // best effort } diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 892aabd2dd84..b1140d0d9c28 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -10,6 +10,7 @@ import { getCurrentScope, hasTracingEnabled, runWithAsyncContext, + setHttpStatus, startTransaction, withScope, } from '@sentry/core'; @@ -105,7 +106,7 @@ export function tracingHandler(): ( // closes setImmediate(() => { addRequestDataToTransaction(transaction, req); - transaction.setHttpStatus(res.statusCode); + setHttpStatus(transaction, res.statusCode); transaction.end(); }); }); diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 42335f7c4ce5..c80265926ce4 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -6,6 +6,7 @@ import { getActiveTransaction, getCurrentScope, getDynamicSamplingContextFromSpan, + setHttpStatus, spanToTraceHeader, startTransaction, } from '@sentry/core'; @@ -117,11 +118,11 @@ export const hapiTracingPlugin = { // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); - if (request.response && isResponseObject(request.response) && transaction) { - transaction.setHttpStatus(request.response.statusCode); - } - if (transaction) { + if (request.response && isResponseObject(request.response)) { + setHttpStatus(transaction, request.response.statusCode); + } + transaction.end(); } diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index e3f6d164d991..47cd73304e06 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -10,6 +10,7 @@ import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, isSentryRequestUrl, + setHttpStatus, spanToJSON, spanToTraceHeader, } from '@sentry/core'; @@ -302,7 +303,7 @@ function _createWrappedRequestMethodFactory( } if (requestSpan) { if (res.statusCode) { - requestSpan.setHttpStatus(res.statusCode); + setHttpStatus(requestSpan, res.statusCode); } requestSpan.updateName( cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', @@ -318,7 +319,7 @@ function _createWrappedRequestMethodFactory( addRequestBreadcrumb('error', data, req); } if (requestSpan) { - requestSpan.setHttpStatus(500); + setHttpStatus(requestSpan, 500); requestSpan.updateName( cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', ); diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index ef875891b3ef..33f1472420f8 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -6,6 +6,7 @@ import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, isSentryRequestUrl, + setHttpStatus, spanToTraceHeader, } from '@sentry/core'; import type { EventProcessor, Integration, Span } from '@sentry/types'; @@ -211,7 +212,7 @@ export class Undici implements Integration { const span = request.__sentry_span__; if (span) { - span.setHttpStatus(response.statusCode); + setHttpStatus(span, response.statusCode); span.end(); } diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index f90e9dfabd77..1ed13e9f28ec 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -8,6 +8,7 @@ import { getDynamicSamplingContextFromSpan, hasTracingEnabled, runWithAsyncContext, + setHttpStatus, spanToJSON, spanToTraceHeader, } from '@sentry/core'; @@ -495,7 +496,7 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui const res = (await origRequestHandler.call(this, request, loadContext)) as Response; if (isResponse(res)) { - transaction.setHttpStatus(res.status); + setHttpStatus(transaction, res.status); } transaction.end(); diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts index d3acd7633132..8f05de780381 100644 --- a/packages/remix/src/utils/serverAdapters/express.ts +++ b/packages/remix/src/utils/serverAdapters/express.ts @@ -1,4 +1,11 @@ -import { getClient, getCurrentHub, getCurrentScope, hasTracingEnabled, runWithAsyncContext } from '@sentry/core'; +import { + getClient, + getCurrentHub, + getCurrentScope, + hasTracingEnabled, + runWithAsyncContext, + setHttpStatus, +} from '@sentry/core'; import { flush } from '@sentry/node'; import type { Transaction } from '@sentry/types'; import { extractRequestData, fill, isString, logger } from '@sentry/utils'; @@ -151,7 +158,7 @@ async function finishSentryProcessing(res: AugmentedExpressResponse): Promise void), encoding?: string | (() => void), cb?: () => void): any { - span?.setHttpStatus(res.statusCode); - span?.end(); + if (span) { + setHttpStatus(span, res.statusCode); + span.end(); + } // eslint-disable-next-line @typescript-eslint/no-floating-promises flush(options.flushTimeout) diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 8fb51d3bf368..b9794553961f 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -1,5 +1,7 @@ import * as domain from 'domain'; +import * as SentryCore from '@sentry/core'; import * as SentryNode from '@sentry/node'; + import type { Event, Integration } from '@sentry/types'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; @@ -16,6 +18,8 @@ import type { } from '../src/gcpfunction/general'; describe('GCPFunction', () => { + const setHttpStatusSpy = jest.spyOn(SentryCore, 'setHttpStatus').mockImplementation(() => {}); + afterEach(() => { // @ts-expect-error see "Why @ts-expect-error" note SentryNode.resetMocks(); @@ -114,7 +118,7 @@ describe('GCPFunction', () => { expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.setHttpStatus).toBeCalledWith(200); + expect(setHttpStatusSpy).toBeCalledWith(SentryNode.fakeSpan, 200); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeSpan.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 5572e64060d3..42f95c905ca5 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -3,6 +3,7 @@ import { getActiveSpan, getCurrentScope, getDynamicSamplingContextFromSpan, + setHttpStatus, spanToTraceHeader, } from '@sentry/core'; import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core'; @@ -191,7 +192,7 @@ async function instrumentHandle( transformPageChunk: addSentryCodeToPage(options), }); if (span) { - span.setHttpStatus(res.status); + setHttpStatus(span, res.status); } return res; }, diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index d2600f02629e..56ba74f7b97f 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -7,6 +7,7 @@ import { getDynamicSamplingContextFromSpan, getRootSpan, hasTracingEnabled, + setHttpStatus, spanToJSON, spanToTraceHeader, startInactiveSpan, @@ -266,7 +267,7 @@ export function xhrCallback( const span = spans[spanId]; if (span && sentryXhrData.status_code !== undefined) { - span.setHttpStatus(sentryXhrData.status_code); + setHttpStatus(span, sentryXhrData.status_code); span.end(); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 8f9da76488ad..2703289b4d0c 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -6,6 +6,7 @@ import { getDynamicSamplingContextFromSpan, getRootSpan, hasTracingEnabled, + setHttpStatus, spanToTraceHeader, startInactiveSpan, } from '@sentry/core'; @@ -53,7 +54,7 @@ export function instrumentFetchRequest( const span = spans[spanId]; if (span) { if (handlerData.response) { - span.setHttpStatus(handlerData.response.status); + setHttpStatus(span, handlerData.response.status); const contentLength = handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length'); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 4a7e1537f394..ae13f42ea698 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -98,6 +98,7 @@ describe('Span', () => { expect((span.getTraceContext() as any).status).toBe('permission_denied'); }); + // TODO (v8): Remove test('setHttpStatus', () => { const span = new Span({}); span.setHttpStatus(404); diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 0dc46467269d..73c2fbdaaaa8 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -332,6 +332,7 @@ export interface Span extends Omit { /** * Sets the status attribute on the current span based on the http code * @param httpStatus http code used to set the status + * @deprecated Use top-level `setHttpStatus()` instead. */ setHttpStatus(httpStatus: number): this; From 2ce3e7d6bca9e68afa824da717fb095b434f8e4f Mon Sep 17 00:00:00 2001 From: Oleh Aloshkin Date: Mon, 29 Jan 2024 11:38:43 +0100 Subject: [PATCH 20/39] fix(react): Fix attachReduxState option (#10381) Co-authored-by: Luca Forstner --- packages/react/src/redux.ts | 5 +++-- packages/react/test/redux.test.ts | 32 +++++++++++++++++-------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 38f99d7af825..fb3ca2fa073f 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { addEventProcessor, getClient, getCurrentScope } from '@sentry/browser'; +import { getClient, getCurrentScope, getGlobalScope } from '@sentry/core'; import type { Scope } from '@sentry/types'; import { addNonEnumerableProperty } from '@sentry/utils'; @@ -97,7 +97,7 @@ function createReduxEnhancer(enhancerOptions?: Partial): return (next: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator => (reducer: Reducer, initialState?: PreloadedState) => { options.attachReduxState && - addEventProcessor((event, hint) => { + getGlobalScope().addEventProcessor((event, hint) => { try { // @ts-expect-error try catch to reduce bundle size if (event.type === undefined && event.contexts.state.state.type === 'redux') { @@ -117,6 +117,7 @@ function createReduxEnhancer(enhancerOptions?: Partial): const newState = reducer(state, action); const scope = getCurrentScope(); + /* Action breadcrumbs */ const transformedAction = options.actionTransformer(action); if (typeof transformedAction !== 'undefined' && transformedAction !== null) { diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index 0ce064365eeb..537c133cd3fd 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -5,24 +5,28 @@ import { createReduxEnhancer } from '../src/redux'; const mockAddBreadcrumb = jest.fn(); const mockSetContext = jest.fn(); +const mockGlobalScopeAddEventProcessor = jest.fn(); -jest.mock('@sentry/browser', () => ({ - ...jest.requireActual('@sentry/browser'), +jest.mock('@sentry/core', () => ({ + ...jest.requireActual('@sentry/core'), getCurrentScope() { return { addBreadcrumb: mockAddBreadcrumb, setContext: mockSetContext, }; }, + getGlobalScope() { + return { + addEventProcessor: mockGlobalScopeAddEventProcessor, + }; + }, addEventProcessor: jest.fn(), })); -const mockAddEventProcessor = Sentry.addEventProcessor as jest.Mock; - afterEach(() => { mockAddBreadcrumb.mockReset(); mockSetContext.mockReset(); - mockAddEventProcessor.mockReset(); + mockGlobalScopeAddEventProcessor.mockReset(); }); describe('createReduxEnhancer', () => { @@ -257,9 +261,9 @@ describe('createReduxEnhancer', () => { Redux.createStore((state = initialState) => state, enhancer); - expect(mockAddEventProcessor).toHaveBeenCalledTimes(1); + expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1); - const callbackFunction = mockAddEventProcessor.mock.calls[0][0]; + const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0]; const mockEvent = { contexts: { @@ -306,7 +310,7 @@ describe('createReduxEnhancer', () => { Redux.createStore((state = initialState) => state, enhancer); - expect(mockAddEventProcessor).toHaveBeenCalledTimes(0); + expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(0); }); it('does not attach when state.type is not redux', () => { @@ -318,9 +322,9 @@ describe('createReduxEnhancer', () => { Redux.createStore((state = initialState) => state, enhancer); - expect(mockAddEventProcessor).toHaveBeenCalledTimes(1); + expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1); - const callbackFunction = mockAddEventProcessor.mock.calls[0][0]; + const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0]; const mockEvent = { contexts: { @@ -353,9 +357,9 @@ describe('createReduxEnhancer', () => { Redux.createStore((state = initialState) => state, enhancer); - expect(mockAddEventProcessor).toHaveBeenCalledTimes(1); + expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1); - const callbackFunction = mockAddEventProcessor.mock.calls[0][0]; + const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0]; const mockEvent = { contexts: { @@ -385,9 +389,9 @@ describe('createReduxEnhancer', () => { Redux.createStore((state = initialState) => state, enhancer); - expect(mockAddEventProcessor).toHaveBeenCalledTimes(1); + expect(mockGlobalScopeAddEventProcessor).toHaveBeenCalledTimes(1); - const callbackFunction = mockAddEventProcessor.mock.calls[0][0]; + const callbackFunction = mockGlobalScopeAddEventProcessor.mock.calls[0][0]; const mockEvent = { type: 'not_redux', From 51b0f5d2a8bc29f47474f86b50f2d7fe73d5a2e0 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 29 Jan 2024 12:21:38 +0100 Subject: [PATCH 21/39] feat(core): Deprecate `spanStatusfromHttpCode` in favour of `getSpanStatusFromHttpCode` (#10361) The old export contained a typo/camel-case inconsistency which I fixed in the new export and I opted to prepend the `get` verb. --- packages/astro/src/index.server.ts | 2 ++ packages/browser/src/index.ts | 2 ++ packages/bun/src/index.ts | 2 ++ packages/core/src/tracing/index.ts | 7 ++++++- packages/core/src/tracing/spanstatus.ts | 15 +++++++++++++-- packages/deno/src/index.ts | 2 ++ packages/node-experimental/src/index.ts | 2 ++ packages/node/src/index.ts | 2 ++ packages/remix/src/index.server.ts | 2 ++ packages/sveltekit/src/server/index.ts | 2 ++ packages/tracing-internal/src/exports/index.ts | 1 + packages/tracing/src/index.ts | 1 + packages/vercel-edge/src/index.ts | 2 ++ 13 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 2197c47381da..8d4bb2a7e371 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -46,7 +46,9 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 0c75bae6e1f9..658dff61716b 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -71,7 +71,9 @@ export { extractTraceparentData, // eslint-disable-next-line deprecation/deprecation getActiveTransaction, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, makeMultiplexedTransport, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 393e534e12ee..9869a52caaa9 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -66,7 +66,9 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 948196dd13c2..d1e1c7f65b44 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -7,7 +7,12 @@ export { Transaction } from './transaction'; export { extractTraceparentData, getActiveTransaction } from './utils'; // eslint-disable-next-line deprecation/deprecation export { SpanStatus } from './spanstatus'; -export { setHttpStatus, spanStatusfromHttpCode } from './spanstatus'; +export { + setHttpStatus, + // eslint-disable-next-line deprecation/deprecation + spanStatusfromHttpCode, + getSpanStatusFromHttpCode, +} from './spanstatus'; export type { SpanStatusType } from './spanstatus'; export { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/core/src/tracing/spanstatus.ts b/packages/core/src/tracing/spanstatus.ts index f38c397f1ae0..aa0d1639a70c 100644 --- a/packages/core/src/tracing/spanstatus.ts +++ b/packages/core/src/tracing/spanstatus.ts @@ -83,7 +83,7 @@ export type SpanStatusType = * @param httpStatus The HTTP response status code. * @returns The span status or unknown_error. */ -export function spanStatusfromHttpCode(httpStatus: number): SpanStatusType { +export function getSpanStatusFromHttpCode(httpStatus: number): SpanStatusType { if (httpStatus < 400 && httpStatus >= 100) { return 'ok'; } @@ -123,6 +123,17 @@ export function spanStatusfromHttpCode(httpStatus: number): SpanStatusType { return 'unknown_error'; } +/** + * Converts a HTTP status code into a {@link SpanStatusType}. + * + * @deprecated Use {@link spanStatusFromHttpCode} instead. + * This export will be removed in v8 as the signature contains a typo. + * + * @param httpStatus The HTTP response status code. + * @returns The span status or unknown_error. + */ +export const spanStatusfromHttpCode = getSpanStatusFromHttpCode; + /** * Sets the Http status attributes on the current span based on the http code. * Additionally, the span's status is updated, depending on the http code. @@ -140,7 +151,7 @@ export function setHttpStatus(span: Span, httpStatus: number): void { // eslint-disable-next-line deprecation/deprecation span.setData('http.response.status_code', httpStatus); - const spanStatus = spanStatusfromHttpCode(httpStatus); + const spanStatus = getSpanStatusFromHttpCode(httpStatus); if (spanStatus !== 'unknown_error') { span.setStatus(spanStatus); } diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index a622df3111e6..f5ed9651bf94 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -65,7 +65,9 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index b19dad4adfd7..2fcb4ee1b166 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -66,7 +66,9 @@ export { Hub, runWithAsyncContext, SDK_VERSION, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, captureCheckIn, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 09fc72a5d382..79edd5eddd89 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -65,7 +65,9 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 62879c11520f..457c0e3a523b 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -49,7 +49,9 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 7d523ee55fd9..eb021c6c76cd 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -43,7 +43,9 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, withScope, diff --git a/packages/tracing-internal/src/exports/index.ts b/packages/tracing-internal/src/exports/index.ts index 8c10b3165608..96cd3b85ac89 100644 --- a/packages/tracing-internal/src/exports/index.ts +++ b/packages/tracing-internal/src/exports/index.ts @@ -8,6 +8,7 @@ export { Span, // eslint-disable-next-line deprecation/deprecation SpanStatus, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, startIdleTransaction, Transaction, diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index 8559188884d7..d789a2b68520 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -78,6 +78,7 @@ export const extractTraceparentData = extractTraceparentDataT; * * `spanStatusfromHttpCode` can be imported from `@sentry/node`, `@sentry/browser`, or your framework SDK */ +// eslint-disable-next-line deprecation/deprecation export const spanStatusfromHttpCode = spanStatusfromHttpCodeT; /** diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index abeb6d54b870..2ff971fde287 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -65,7 +65,9 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation spanStatusfromHttpCode, + getSpanStatusFromHttpCode, // eslint-disable-next-line deprecation/deprecation trace, withScope, From b3af0dc4285daece142a702a907f0ecfdda1bd36 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 29 Jan 2024 15:35:49 +0100 Subject: [PATCH 22/39] ref: Read propagation context off of scope and isolation scope when propagating and applying trace context (#10297) --- .../baggage-header-assign/test.ts | 27 +++++++-- .../test.ts | 2 +- .../baggage-other-vendors/test.ts | 2 +- packages/core/src/baseclient.ts | 12 ++-- packages/core/src/tracing/trace.ts | 2 +- .../core/src/utils/applyScopeDataToEvent.ts | 13 ++--- packages/core/test/lib/prepareEvent.test.ts | 17 ++---- packages/core/test/lib/scope.test.ts | 16 ++--- packages/core/test/lib/tracing/trace.test.ts | 1 + packages/hub/test/scope.test.ts | 2 - .../test/integration/transactions.test.ts | 5 -- .../node-experimental/test/sdk/scope.test.ts | 16 ++--- packages/node/src/integrations/http.ts | 52 ++++++++--------- .../node/src/integrations/undici/index.ts | 29 ++++++---- packages/node/test/handlers.test.ts | 1 + packages/opentelemetry/src/propagator.ts | 2 +- packages/opentelemetry/src/spanExporter.ts | 6 +- .../test/integration/scope.test.ts | 2 - .../test/integration/transactions.test.ts | 5 -- .../opentelemetry/test/propagator.test.ts | 7 ++- .../src/browser/browserTracingIntegration.ts | 57 +++++++----------- .../src/browser/browsertracing.ts | 58 +++++++------------ .../tracing-internal/src/browser/request.ts | 34 ++++++----- packages/tracing-internal/src/common/fetch.ts | 21 +++---- packages/types/src/tracing.ts | 35 ++++++++++- packages/utils/src/tracing.ts | 41 +++++++------ packages/utils/test/tracing.test.ts | 9 +++ 27 files changed, 239 insertions(+), 235 deletions(-) create mode 100644 packages/utils/test/tracing.test.ts diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts index 190f458ea76c..4ec29414868c 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts @@ -5,7 +5,7 @@ afterAll(() => { cleanupChildProcesses(); }); -test('Should not overwrite baggage if the incoming request already has Sentry baggage data.', async () => { +test('Should overwrite baggage if the incoming request already has Sentry baggage data but no sentry-trace', async () => { const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { @@ -13,7 +13,7 @@ test('Should not overwrite baggage if the incoming request already has Sentry ba }); expect(response).toBeDefined(); - expect(response).toMatchObject({ + expect(response).not.toMatchObject({ test_data: { host: 'somewhere.not.sentry', baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', @@ -25,7 +25,7 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '', + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', }); @@ -38,11 +38,28 @@ test('Should propagate sentry trace baggage data from an incoming to an outgoing }); }); -test('Should not propagate baggage if sentry-trace header is present in incoming request but no baggage header', async () => { +test('Should not propagate baggage data from an incoming to an outgoing request if sentry-trace is faulty.', async () => { const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { 'sentry-trace': '', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + }); + + expect(response).toBeDefined(); + expect(response).not.toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, + }); +}); + +test('Should not propagate baggage if sentry-trace header is present in incoming request but no baggage header', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', }); expect(response).toBeDefined(); @@ -57,7 +74,7 @@ test('Should not propagate baggage and ignore original 3rd party baggage entries const runner = createRunner(__dirname, '..', 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '', + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', baggage: 'foo=bar', }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts index 1accfce01316..b7b6c08c0f3e 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts @@ -9,7 +9,7 @@ test('should ignore sentry-values in `baggage` header of a third party vendor an const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '', + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', baggage: 'sentry-release=2.1.0,sentry-environment=myEnv', }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts index bc93b2886a19..41b2b9d7cf19 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts @@ -9,7 +9,7 @@ test('should merge `baggage` header of a third party vendor with the Sentry DSC const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { - 'sentry-trace': '', + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', }); diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index c64c5cf92c0d..b3eb10f686d7 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -20,7 +20,6 @@ import type { MetricsAggregator, Outcome, ParameterizedString, - PropagationContext, SdkMetadata, Session, SessionAggregates, @@ -662,13 +661,14 @@ export abstract class BaseClient implements Client { return evt; } - // If a trace context is not set on the event, we use the propagationContext set on the event to - // generate a trace context. If the propagationContext does not have a dynamic sampling context, we - // also generate one for it. - const { propagationContext } = evt.sdkProcessingMetadata || {}; + const propagationContext = { + ...isolationScope.getPropagationContext(), + ...(scope ? scope.getPropagationContext() : undefined), + }; + const trace = evt.contexts && evt.contexts.trace; if (!trace && propagationContext) { - const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext as PropagationContext; + const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext; evt.contexts = { trace: { trace_id, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 885cbd7c9d08..ad1b95345ef9 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -237,7 +237,7 @@ export function continueTrace( const transactionContext: Partial = { ...traceparentData, metadata: dropUndefinedKeys({ - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + dynamicSamplingContext, }), }; diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index bdf60497cff1..92a5e6747cd8 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -1,4 +1,4 @@ -import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types'; +import type { Breadcrumb, Event, ScopeData, Span } from '@sentry/types'; import { arrayify, dropUndefinedKeys } from '@sentry/utils'; import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext'; import { getRootSpan } from './getRootSpan'; @@ -8,7 +8,7 @@ import { spanToJSON, spanToTraceContext } from './spanUtils'; * Applies data from the scope to the event and runs all event processors on it. */ export function applyScopeDataToEvent(event: Event, data: ScopeData): void { - const { fingerprint, span, breadcrumbs, sdkProcessingMetadata, propagationContext } = data; + const { fingerprint, span, breadcrumbs, sdkProcessingMetadata } = data; // Apply general data applyDataToEvent(event, data); @@ -22,7 +22,7 @@ export function applyScopeDataToEvent(event: Event, data: ScopeData): void { applyFingerprintToEvent(event, fingerprint); applyBreadcrumbsToEvent(event, breadcrumbs); - applySdkMetadataToEvent(event, sdkProcessingMetadata, propagationContext); + applySdkMetadataToEvent(event, sdkProcessingMetadata); } /** Merge data of two scopes together. */ @@ -163,15 +163,10 @@ function applyBreadcrumbsToEvent(event: Event, breadcrumbs: Breadcrumb[]): void event.breadcrumbs = mergedBreadcrumbs.length ? mergedBreadcrumbs : undefined; } -function applySdkMetadataToEvent( - event: Event, - sdkProcessingMetadata: ScopeData['sdkProcessingMetadata'], - propagationContext: PropagationContext, -): void { +function applySdkMetadataToEvent(event: Event, sdkProcessingMetadata: ScopeData['sdkProcessingMetadata']): void { event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, ...sdkProcessingMetadata, - propagationContext: propagationContext, }; } diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 22f62bbcf89c..cb3c616b7390 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -228,12 +228,7 @@ describe('prepareEvent', () => { event_id: expect.any(String), environment: 'production', message: 'foo', - sdkProcessingMetadata: { - propagationContext: { - spanId: expect.any(String), - traceId: expect.any(String), - }, - }, + sdkProcessingMetadata: {}, }); }); @@ -309,16 +304,15 @@ describe('prepareEvent', () => { user: { id: '1', email: 'test@example.com' }, tags: { tag1: 'aa', tag2: 'aa' }, extra: { extra1: 'aa', extra2: 'aa' }, - contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + contexts: { + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + }, fingerprint: ['dd', 'aa'], breadcrumbs: [breadcrumb4, breadcrumb2, breadcrumb3, breadcrumb1], sdkProcessingMetadata: { aa: 'aa', bb: 'bb', - propagationContext: { - spanId: '1', - traceId: '1', - }, }, }); }); @@ -382,7 +376,6 @@ describe('prepareEvent', () => { sdkProcessingMetadata: { aa: 'aa', bb: 'bb', - propagationContext: isolationScope.getPropagationContext(), }, }); }); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index e4510ef58e3b..4b4242ce7dc6 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -132,12 +132,7 @@ describe('Scope', () => { expect(event).toEqual({ message: 'foo', - sdkProcessingMetadata: { - propagationContext: { - spanId: expect.any(String), - traceId: expect.any(String), - }, - }, + sdkProcessingMetadata: {}, }); }); @@ -166,15 +161,14 @@ describe('Scope', () => { user: { id: '1', email: 'test@example.com' }, tags: { tag1: 'aa', tag2: 'aa' }, extra: { extra1: 'aa', extra2: 'aa' }, - contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + contexts: { + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + }, fingerprint: ['dd', 'aa'], breadcrumbs: [breadcrumb2, breadcrumb1], sdkProcessingMetadata: { aa: 'aa', - propagationContext: { - spanId: '1', - traceId: '1', - }, }, }); }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index cc11793fc07e..f2103891b4f5 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -489,6 +489,7 @@ describe('continueTrace', () => { const scope = getCurrentScope(); expect(scope.getPropagationContext()).toEqual({ + dsc: {}, // DSC should be an empty object (frozen), because there was an incoming trace sampled: false, parentSpanId: '1121201211212012', spanId: expect.any(String), diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index b8cfaf1914e0..08f4a73abcb2 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -261,8 +261,6 @@ describe('Scope', () => { expect(processedEvent!.contexts).toEqual({ os: { id: '1' } }); expect(processedEvent!.sdkProcessingMetadata).toEqual({ dogs: 'are great!', - // @ts-expect-error accessing private property for test - propagationContext: scope._propagationContext, }); }); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index b1038a53a276..929b286452f3 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -108,11 +108,6 @@ describe('Integration | Transactions', () => { trace_id: expect.any(String), transaction: 'test name', }), - propagationContext: { - sampled: undefined, - spanId: expect.any(String), - traceId: expect.any(String), - }, sampleRate: 1, source: 'task', spanMetadata: expect.any(Object), diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts index e3919654e920..076188d9c9b7 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -130,12 +130,7 @@ describe('Unit | Scope', () => { event_id: expect.any(String), environment: 'production', message: 'foo', - sdkProcessingMetadata: { - propagationContext: { - spanId: expect.any(String), - traceId: expect.any(String), - }, - }, + sdkProcessingMetadata: {}, }); }); @@ -213,16 +208,15 @@ describe('Unit | Scope', () => { user: { id: '1', email: 'test@example.com' }, tags: { tag1: 'aa', tag2: 'aa' }, extra: { extra1: 'aa', extra2: 'aa' }, - contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + contexts: { + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + }, fingerprint: ['dd', 'aa'], breadcrumbs: [breadcrumb4, breadcrumb2, breadcrumb3, breadcrumb1], sdkProcessingMetadata: { aa: 'aa', bb: 'bb', - propagationContext: { - spanId: '1', - traceId: '1', - }, }, }); }); diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 47cd73304e06..de013541257e 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,6 +1,7 @@ import type * as http from 'http'; import type * as https from 'https'; import type { Hub } from '@sentry/core'; +import { getIsolationScope } from '@sentry/core'; import { addBreadcrumb, getActiveSpan, @@ -14,13 +15,7 @@ import { spanToJSON, spanToTraceHeader, } from '@sentry/core'; -import type { - DynamicSamplingContext, - EventProcessor, - Integration, - SanitizedRequestData, - TracePropagationTargets, -} from '@sentry/types'; +import type { EventProcessor, Integration, SanitizedRequestData, TracePropagationTargets } from '@sentry/types'; import { LRUMap, dynamicSamplingContextToSentryBaggageHeader, @@ -251,13 +246,15 @@ function _createWrappedRequestMethodFactory( // eslint-disable-next-line deprecation/deprecation const rawRequestUrl = extractRawUrl(requestOptions); const requestUrl = extractUrl(requestOptions); + const client = getClient(); // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method - if (isSentryRequestUrl(requestUrl, getClient())) { + if (isSentryRequestUrl(requestUrl, client)) { return originalRequestMethod.apply(httpModule, requestArgs); } const scope = getCurrentScope(); + const isolationScope = getIsolationScope(); const parentSpan = getActiveSpan(); const data = getRequestSpanData(requestUrl, requestOptions); @@ -272,19 +269,24 @@ function _createWrappedRequestMethodFactory( }) : undefined; - if (shouldAttachTraceData(rawRequestUrl)) { - if (requestSpan) { - const sentryTraceHeader = spanToTraceHeader(requestSpan); - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan); - addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); - } else { - const client = getClient(); - const { traceId, sampled, dsc } = scope.getPropagationContext(); - const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled); - const dynamicSamplingContext = - dsc || (client ? getDynamicSamplingContextFromClient(traceId, client, scope) : undefined); - addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); - } + if (client && shouldAttachTraceData(rawRequestUrl)) { + const { traceId, spanId, sampled, dsc } = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; + + const sentryTraceHeader = requestSpan + ? spanToTraceHeader(requestSpan) + : generateSentryTraceHeader(traceId, spanId, sampled); + + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( + dsc || + (requestSpan + ? getDynamicSamplingContextFromSpan(requestSpan) + : getDynamicSamplingContextFromClient(traceId, client, scope)), + ); + + addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader); } else { DEBUG_BUILD && logger.log( @@ -334,7 +336,7 @@ function addHeadersToRequestOptions( requestOptions: RequestOptions, requestUrl: string, sentryTraceHeader: string, - dynamicSamplingContext: Partial | undefined, + sentryBaggageHeader: string | undefined, ): void { // Don't overwrite sentry-trace and baggage header if it's already set. const headers = requestOptions.headers || {}; @@ -344,15 +346,13 @@ function addHeadersToRequestOptions( DEBUG_BUILD && logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `); - const sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - const sentryBaggageHeader = - sentryBaggage && sentryBaggage.length > 0 ? normalizeBaggageHeader(requestOptions, sentryBaggage) : undefined; requestOptions.headers = { ...requestOptions.headers, 'sentry-trace': sentryTraceHeader, // Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined - ...(sentryBaggageHeader && { baggage: sentryBaggageHeader }), + ...(sentryBaggageHeader && + sentryBaggageHeader.length > 0 && { baggage: normalizeBaggageHeader(requestOptions, sentryBaggageHeader) }), }; } diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 33f1472420f8..31178d95aaa8 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -5,6 +5,7 @@ import { getCurrentScope, getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, + getIsolationScope, isSentryRequestUrl, setHttpStatus, spanToTraceHeader, @@ -158,6 +159,7 @@ export class Undici implements Integration { const clientOptions = client.getOptions(); const scope = getCurrentScope(); + const isolationScope = getIsolationScope(); const parentSpan = getActiveSpan(); const span = this._shouldCreateSpan(stringUrl) ? createRequestSpan(parentSpan, request, stringUrl) : undefined; @@ -181,18 +183,21 @@ export class Undici implements Integration { }; if (shouldAttachTraceData(stringUrl)) { - if (span) { - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - - setHeadersOnRequest(request, spanToTraceHeader(span), sentryBaggageHeader); - } else { - const { traceId, sampled, dsc } = scope.getPropagationContext(); - const sentryTrace = generateSentryTraceHeader(traceId, undefined, sampled); - const dynamicSamplingContext = dsc || getDynamicSamplingContextFromClient(traceId, client, scope); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - setHeadersOnRequest(request, sentryTrace, sentryBaggageHeader); - } + const { traceId, spanId, sampled, dsc } = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; + + const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled); + + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( + dsc || + (span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromClient(traceId, client, scope)), + ); + + setHeadersOnRequest(request, sentryTraceHeader, sentryBaggageHeader); } }; diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index d9001f6ffdd9..8438a3fc2acd 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -270,6 +270,7 @@ describe('tracingHandler', () => { parentSpanId: '1121201211212012', spanId: expect.any(String), sampled: false, + dsc: {}, // There is an incoming trace but no baggage header, so the DSC must be frozen (empty object) }); // since we have no tracesSampler defined, the default behavior (inherit if possible) applies diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 0768541fbfb9..bbeb2744e501 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -83,7 +83,7 @@ function getDsc( context: Context, propagationContext: PropagationContext, traceId: string | undefined, -): DynamicSamplingContext | undefined { +): Partial | undefined { // If we have a DSC on the propagation context, we just use it if (propagationContext.dsc) { return propagationContext.dsc; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index ca2f1deb4fc0..0d57d1009e31 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -4,7 +4,7 @@ import { ExportResultCode } from '@opentelemetry/core'; import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { flush, getCurrentScope } from '@sentry/core'; -import type { DynamicSamplingContext, Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; +import type { Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; import { logger } from '@sentry/utils'; import { getCurrentHub } from './custom/hub'; @@ -158,9 +158,7 @@ function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransact const parentSpanId = span.parentSpanId; const parentSampled = span.attributes[InternalSentrySemanticAttributes.PARENT_SAMPLED] as boolean | undefined; - const dynamicSamplingContext: DynamicSamplingContext | undefined = scope - ? scope.getPropagationContext().dsc - : undefined; + const dynamicSamplingContext = scope ? scope.getPropagationContext().dsc : undefined; const { op, description, tags, data, origin, source } = getSpanData(span); const metadata = getSpanMetadata(span); diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts index 6ab7c6d35621..d0234b27a140 100644 --- a/packages/opentelemetry/test/integration/scope.test.ts +++ b/packages/opentelemetry/test/integration/scope.test.ts @@ -69,7 +69,6 @@ describe('Integration | Scope', () => { ? { span_id: spanId, trace_id: traceId, - parent_span_id: undefined, } : expect.any(Object), }), @@ -190,7 +189,6 @@ describe('Integration | Scope', () => { ? { span_id: spanId1, trace_id: traceId1, - parent_span_id: undefined, } : expect.any(Object), }), diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index a87816691a45..5c4742df5f97 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -106,11 +106,6 @@ describe('Integration | Transactions', () => { trace_id: expect.any(String), transaction: 'test name', }), - propagationContext: { - sampled: undefined, - spanId: expect.any(String), - traceId: expect.any(String), - }, sampleRate: 1, source: 'task', spanMetadata: expect.any(Object), diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index 1663021b6224..a4eb98ab2126 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -310,6 +310,7 @@ describe('SentryPropagator', () => { parentSpanId: '6e0c63257de34c92', spanId: expect.any(String), traceId: 'd4cda95b652f4a1592b449d5929fda1b', + dsc: {}, // Frozen DSC }); // Ensure spanId !== parentSpanId - it should be a new random ID @@ -321,19 +322,21 @@ describe('SentryPropagator', () => { carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); expect(getPropagationContextFromContext(context)).toEqual({ - sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), }); }); it('sets defined dynamic sampling context on context', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; const baggage = 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); expect(getPropagationContextFromContext(context)).toEqual({ - sampled: undefined, + sampled: true, + parentSpanId: expect.any(String), spanId: expect.any(String), traceId: expect.any(String), // Note: This is not automatically taken from the DSC (in reality, this should be aligned) dsc: { diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 8184ad058039..59d15d9588ee 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -7,7 +7,6 @@ import { TRACING_DEFAULTS, addTracingExtensions, getActiveTransaction, - spanIsSampled, spanToJSON, startIdleTransaction, } from '@sentry/core'; @@ -199,23 +198,27 @@ export const _browserTracingIntegration = ((_options: Partial { _collectWebVitals(); addPerformanceEntries(transaction); diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index 2d8ffd9af135..83897724fa5e 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -5,8 +5,6 @@ import { TRACING_DEFAULTS, addTracingExtensions, getActiveTransaction, - spanIsSampled, - spanToJSON, startIdleTransaction, } from '@sentry/core'; import type { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; @@ -310,23 +308,27 @@ export class BrowserTracing implements Integration { const isPageloadTransaction = context.op === 'pageload'; - const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : ''; - const baggage = isPageloadTransaction ? getMetaContent('baggage') : ''; - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( - sentryTrace, - baggage, - ); - - const expandedContext: TransactionContext = { - ...context, - ...traceparentData, - metadata: { - // eslint-disable-next-line deprecation/deprecation - ...context.metadata, - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - }, - trimEnd: true, - }; + let expandedContext: TransactionContext; + if (isPageloadTransaction) { + const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : ''; + const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined; + const { traceparentData, dynamicSamplingContext } = tracingContextFromHeaders(sentryTrace, baggage); + expandedContext = { + ...context, + ...traceparentData, + metadata: { + // eslint-disable-next-line deprecation/deprecation + ...context.metadata, + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }, + trimEnd: true, + }; + } else { + expandedContext = { + ...context, + trimEnd: true, + }; + } const modifiedContext = typeof beforeNavigate === 'function' ? beforeNavigate(expandedContext) : expandedContext; @@ -378,24 +380,6 @@ export class BrowserTracing implements Integration { } } - // eslint-disable-next-line deprecation/deprecation - const scope = hub.getScope(); - - // If it's a pageload and there is a meta tag set - // use the traceparentData as the propagation context - if (isPageloadTransaction && traceparentData) { - scope.setPropagationContext(propagationContext); - } else { - // Navigation transactions should set a new propagation context based on the - // created idle transaction. - scope.setPropagationContext({ - traceId: idleTransaction.spanContext().traceId, - spanId: idleTransaction.spanContext().spanId, - parentSpanId: spanToJSON(idleTransaction).parent_span_id, - sampled: spanIsSampled(idleTransaction), - }); - } - idleTransaction.registerBeforeFinishCallback(transaction => { this._collectWebVitals(); addPerformanceEntries(transaction); diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 56ba74f7b97f..557256774f73 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -5,7 +5,7 @@ import { getCurrentScope, getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, - getRootSpan, + getIsolationScope, hasTracingEnabled, setHttpStatus, spanToJSON, @@ -277,6 +277,7 @@ export function xhrCallback( } const scope = getCurrentScope(); + const isolationScope = getIsolationScope(); const span = shouldCreateSpanResult ? startInactiveSpan({ @@ -296,21 +297,22 @@ export function xhrCallback( spans[xhr.__sentry_xhr_span_id__] = span; } - if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) { - if (span) { - const transaction = span && getRootSpan(span); - const dynamicSamplingContext = transaction && getDynamicSamplingContextFromSpan(transaction); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - setHeaderOnXhr(xhr, spanToTraceHeader(span), sentryBaggageHeader); - } else { - const client = getClient(); - const { traceId, sampled, dsc } = scope.getPropagationContext(); - const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled); - const dynamicSamplingContext = - dsc || (client ? getDynamicSamplingContextFromClient(traceId, client, scope) : undefined); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader); - } + const client = getClient(); + + if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url) && client) { + const { traceId, spanId, sampled, dsc } = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; + + const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled); + + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( + dsc || + (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client, scope)), + ); + + setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader); } return span; diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 2703289b4d0c..3d973b0055b9 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -4,7 +4,7 @@ import { getCurrentScope, getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, - getRootSpan, + getIsolationScope, hasTracingEnabled, setHttpStatus, spanToTraceHeader, @@ -134,18 +134,19 @@ export function addTracingHeadersToFetchRequest( // eslint-disable-next-line deprecation/deprecation const span = requestSpan || scope.getSpan(); - const transaction = span && getRootSpan(span); + const isolationScope = getIsolationScope(); - const { traceId, sampled, dsc } = scope.getPropagationContext(); + const { traceId, spanId, sampled, dsc } = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; - const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); - const dynamicSamplingContext = transaction - ? getDynamicSamplingContextFromSpan(transaction) - : dsc - ? dsc - : getDynamicSamplingContextFromClient(traceId, client, scope); + const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( + dsc || + (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client, scope)), + ); const headers = options.headers || diff --git a/packages/types/src/tracing.ts b/packages/types/src/tracing.ts index 7c5b02c45596..701f9930d314 100644 --- a/packages/types/src/tracing.ts +++ b/packages/types/src/tracing.ts @@ -2,10 +2,43 @@ import type { DynamicSamplingContext } from './envelope'; export type TracePropagationTargets = (string | RegExp)[]; +/** + * `PropagationContext` represents the data from an incoming trace. It should be constructed from incoming trace data, + * usually represented by `sentry-trace` and `baggage` HTTP headers. + * + * There is always a propagation context present in the SDK (or rather on Scopes), holding at least a `traceId`. This is + * to ensure that there is always a trace we can attach events onto, even if performance monitoring is disabled. If + * there was no incoming `traceId`, the `traceId` will be generated by the current SDK. + */ export interface PropagationContext { + /** + * Either represents the incoming `traceId` or the `traceId` generated by the current SDK, if there was no incoming trace. + */ traceId: string; + /** + * Represents the execution context of the current SDK. This acts as a fallback value to associate events with a + * particular execution context when performance monitoring is disabled. + * + * The ID of a current span (if one exists) should have precedence over this value when propagating trace data. + */ spanId: string; + /** + * Represents the sampling decision of the incoming trace. + * + * The current SDK should not modify this value! + */ sampled?: boolean; + /** + * The `parentSpanId` denotes the ID of the incoming client span. If there is no `parentSpanId` on the propagation + * context, it means that the the incoming trace didn't come from a span. + * + * The current SDK should not modify this value! + */ parentSpanId?: string; - dsc?: DynamicSamplingContext; + /** + * An undefined dsc in the propagation context means that the current SDK invocation is the head of trace and still free to modify and set the DSC for outgoing requests. + * + * The current SDK should not modify this value! + */ + dsc?: Partial; } diff --git a/packages/utils/src/tracing.ts b/packages/utils/src/tracing.ts index 438af21ac744..b0c25e7f1935 100644 --- a/packages/utils/src/tracing.ts +++ b/packages/utils/src/tracing.ts @@ -1,4 +1,4 @@ -import type { DynamicSamplingContext, PropagationContext, TraceparentData } from '@sentry/types'; +import type { PropagationContext, TraceparentData } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext } from './baggage'; import { uuid4 } from './misc'; @@ -59,25 +59,28 @@ export function tracingContextFromHeaders( const { traceId, parentSpanId, parentSampled } = traceparentData || {}; - const propagationContext: PropagationContext = { - traceId: traceId || uuid4(), - spanId: uuid4().substring(16), - sampled: parentSampled, - }; - - if (parentSpanId) { - propagationContext.parentSpanId = parentSpanId; - } - - if (dynamicSamplingContext) { - propagationContext.dsc = dynamicSamplingContext as DynamicSamplingContext; + if (!traceparentData) { + return { + traceparentData, + dynamicSamplingContext: undefined, + propagationContext: { + traceId: traceId || uuid4(), + spanId: uuid4().substring(16), + }, + }; + } else { + return { + traceparentData, + dynamicSamplingContext: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + propagationContext: { + traceId: traceId || uuid4(), + parentSpanId: parentSpanId || uuid4().substring(16), + spanId: uuid4().substring(16), + sampled: parentSampled, + dsc: dynamicSamplingContext || {}, // If we have traceparent data but no DSC it means we are not head of trace and we must freeze it + }, + }; } - - return { - traceparentData, - dynamicSamplingContext, - propagationContext, - }; } /** diff --git a/packages/utils/test/tracing.test.ts b/packages/utils/test/tracing.test.ts new file mode 100644 index 000000000000..2e7cc4d3d5a5 --- /dev/null +++ b/packages/utils/test/tracing.test.ts @@ -0,0 +1,9 @@ +import { tracingContextFromHeaders } from '../src/tracing'; + +describe('tracingContextFromHeaders()', () => { + it('should produce a frozen baggage (empty object) when there is an incoming trace but no baggage header', () => { + const tracingContext = tracingContextFromHeaders('12312012123120121231201212312012-1121201211212012-1', undefined); + expect(tracingContext.dynamicSamplingContext).toEqual({}); + expect(tracingContext.propagationContext.dsc).toEqual({}); + }); +}); From 87ad0fe8898c4bdf3589b01732fef855b5171984 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 29 Jan 2024 15:40:11 +0100 Subject: [PATCH 23/39] test(node): Add `hapi` auto instrumentation test for `@sentry/node-experimental` (#10382) Ref #9907 This actually tests `@hapi/hapi@20.3.0` because the latest v21 is not supported by the otel auto-instrumentation. --- .../node-integration-tests/package.json | 1 + .../node-integration-tests/src/index.ts | 8 + .../tracing-experimental/hapi/scenario.js | 35 ++++ .../suites/tracing-experimental/hapi/test.ts | 35 ++++ yarn.lock | 193 +++++++++++++++++- 5 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js create mode 100644 dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index b0ec56dea204..a3b1a5c968d5 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -27,6 +27,7 @@ "test:watch": "yarn test --watch" }, "dependencies": { + "@hapi/hapi": "^20.3.0", "@prisma/client": "3.15.2", "@sentry/node": "7.98.0", "@sentry/tracing": "7.98.0", diff --git a/dev-packages/node-integration-tests/src/index.ts b/dev-packages/node-integration-tests/src/index.ts index 423b1ac3d93f..aba4e76caa7e 100644 --- a/dev-packages/node-integration-tests/src/index.ts +++ b/dev-packages/node-integration-tests/src/index.ts @@ -29,3 +29,11 @@ export function startExpressServerAndSendPortToRunner(app: Express): void { console.log(`{"port":${address.port}}`); }); } + +/** + * Sends the port to the runner + */ +export function sendPortToRunner(port: number): void { + // eslint-disable-next-line no-console + console.log(`{"port":${port}}`); +} diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js new file mode 100644 index 000000000000..5f2c898fad60 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js @@ -0,0 +1,35 @@ +const { loggingTransport, sendPortToRunner } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node-experimental'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +const Hapi = require('@hapi/hapi'); + +const port = 5999; + +const init = async () => { + const server = Hapi.server({ + host: 'localhost', + port, + }); + + server.route({ + method: 'GET', + path: '/', + handler: (_request, _h) => { + return 'Hello World!'; + }, + }); + + await server.start(); + + sendPortToRunner(port); +}; + +init(); diff --git a/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts new file mode 100644 index 000000000000..148bf83bb397 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing-experimental/hapi/test.ts @@ -0,0 +1,35 @@ +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +jest.setTimeout(20000); + +conditionalTest({ min: 14 })('hapi auto-instrumentation', () => { + afterAll(async () => { + cleanupChildProcesses(); + }); + + const EXPECTED_TRANSACTION = { + transaction: 'GET /', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'http.route': '/', + 'http.method': 'GET', + 'hapi.type': 'router', + 'sentry.origin': 'manual', + 'sentry.op': 'http', + }), + description: 'GET /', + op: 'http', + status: 'ok', + }), + ]), + }; + + test('CJS - should auto-instrument `@hapi/hapi` package.', done => { + createRunner(__dirname, 'scenario.js') + .expect({ transaction: EXPECTED_TRANSACTION }) + .start(done) + .makeRequest('get', '/'); + }); +}); diff --git a/yarn.lock b/yarn.lock index ecbf1157aa3f..bcbeead21f52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3541,6 +3541,21 @@ "@hapi/boom" "9.x.x" "@hapi/hoek" "9.x.x" +"@hapi/accept@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523" + integrity sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/hoek" "9.x.x" + +"@hapi/ammo@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-5.0.1.tgz#9d34560f5c214eda563d838c01297387efaab490" + integrity sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA== + dependencies: + "@hapi/hoek" "9.x.x" + "@hapi/b64@5.x.x": version "5.0.0" resolved "https://registry.yarnpkg.com/@hapi/b64/-/b64-5.0.0.tgz#b8210cbd72f4774985e78569b77e97498d24277d" @@ -3555,18 +3570,59 @@ dependencies: "@hapi/hoek" "9.x.x" -"@hapi/boom@^9.0.0": +"@hapi/boom@^9.0.0", "@hapi/boom@^9.1.0": version "9.1.4" resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.4.tgz#1f9dad367c6a7da9f8def24b4a986fc5a7bd9db6" integrity sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw== dependencies: "@hapi/hoek" "9.x.x" +"@hapi/bounce@2.x.x", "@hapi/bounce@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@hapi/bounce/-/bounce-2.0.0.tgz#e6ef56991c366b1e2738b2cd83b01354d938cf3d" + integrity sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/hoek" "9.x.x" + "@hapi/bourne@2.x.x": version "2.1.0" resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.1.0.tgz#66aff77094dc3080bd5df44ec63881f2676eb020" integrity sha512-i1BpaNDVLJdRBEKeJWkVO6tYX6DMFBuwMhSuWqLsY4ufeTKGVuV5rBsUhxPayXqnnWHgXUAmWK16H/ykO5Wj4Q== +"@hapi/call@^8.0.0": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@hapi/call/-/call-8.0.1.tgz#9e64cd8ba6128eb5be6e432caaa572b1ed8cd7c0" + integrity sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/hoek" "9.x.x" + +"@hapi/catbox-memory@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@hapi/catbox-memory/-/catbox-memory-5.0.1.tgz#cb63fca0ded01d445a2573b38eb2688df67f70ac" + integrity sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/hoek" "9.x.x" + +"@hapi/catbox@^11.1.1": + version "11.1.1" + resolved "https://registry.yarnpkg.com/@hapi/catbox/-/catbox-11.1.1.tgz#d277e2d5023fd69cddb33d05b224ea03065fec0c" + integrity sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/hoek" "9.x.x" + "@hapi/podium" "4.x.x" + "@hapi/validate" "1.x.x" + +"@hapi/content@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@hapi/content/-/content-5.0.2.tgz#ae57954761de570392763e64cdd75f074176a804" + integrity sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/cryptiles@5.x.x": version "5.1.0" resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-5.1.0.tgz#655de4cbbc052c947f696148c83b187fc2be8f43" @@ -3574,17 +3630,55 @@ dependencies: "@hapi/boom" "9.x.x" +"@hapi/file@2.x.x": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9" + integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ== + +"@hapi/hapi@^20.3.0": + version "20.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hapi/-/hapi-20.3.0.tgz#1d620005afeebcb2c8170352286a4664b0107c15" + integrity sha512-zvPSRvaQyF3S6Nev9aiAzko2/hIFZmNSJNcs07qdVaVAvj8dGJSV4fVUuQSnufYJAGiSau+U5LxMLhx79se5WA== + dependencies: + "@hapi/accept" "^5.0.1" + "@hapi/ammo" "^5.0.1" + "@hapi/boom" "^9.1.0" + "@hapi/bounce" "^2.0.0" + "@hapi/call" "^8.0.0" + "@hapi/catbox" "^11.1.1" + "@hapi/catbox-memory" "^5.0.0" + "@hapi/heavy" "^7.0.1" + "@hapi/hoek" "^9.0.4" + "@hapi/mimos" "^6.0.0" + "@hapi/podium" "^4.1.1" + "@hapi/shot" "^5.0.5" + "@hapi/somever" "^3.0.0" + "@hapi/statehood" "^7.0.3" + "@hapi/subtext" "^7.1.0" + "@hapi/teamwork" "^5.1.0" + "@hapi/topo" "^5.0.0" + "@hapi/validate" "^1.1.1" + +"@hapi/heavy@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@hapi/heavy/-/heavy-7.0.1.tgz#73315ae33b6e7682a0906b7a11e8ca70e3045874" + integrity sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/hoek" "9.x.x" + "@hapi/validate" "1.x.x" + "@hapi/hoek@9.x.x": version "9.2.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== -"@hapi/hoek@^9.0.0": +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.0.4": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== -"@hapi/iron@^6.0.0": +"@hapi/iron@6.x.x", "@hapi/iron@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@hapi/iron/-/iron-6.0.0.tgz#ca3f9136cda655bdd6028de0045da0de3d14436f" integrity sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw== @@ -3595,7 +3689,34 @@ "@hapi/cryptiles" "5.x.x" "@hapi/hoek" "9.x.x" -"@hapi/podium@^4.1.3": +"@hapi/mimos@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@hapi/mimos/-/mimos-6.0.0.tgz#daa523d9c07222c7e8860cb7c9c5501fd6506484" + integrity sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg== + dependencies: + "@hapi/hoek" "9.x.x" + mime-db "1.x.x" + +"@hapi/nigel@4.x.x": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@hapi/nigel/-/nigel-4.0.2.tgz#8f84ef4bca4fb03b2376463578f253b0b8e863c4" + integrity sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw== + dependencies: + "@hapi/hoek" "^9.0.4" + "@hapi/vise" "^4.0.0" + +"@hapi/pez@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/pez/-/pez-5.1.0.tgz#c03a5e01f8be01cfabc4c0017631e619586321c1" + integrity sha512-YfB0btnkLB3lb6Ry/1KifnMPBm5ZPfaAHWFskzOMAgDgXgcBgA+zjpIynyEiBfWEz22DBT8o1e2tAaBdlt8zbw== + dependencies: + "@hapi/b64" "5.x.x" + "@hapi/boom" "9.x.x" + "@hapi/content" "^5.0.2" + "@hapi/hoek" "9.x.x" + "@hapi/nigel" "4.x.x" + +"@hapi/podium@4.x.x", "@hapi/podium@^4.1.1", "@hapi/podium@^4.1.3": version "4.1.3" resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.3.tgz#91e20838fc2b5437f511d664aabebbb393578a26" integrity sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g== @@ -3604,7 +3725,49 @@ "@hapi/teamwork" "5.x.x" "@hapi/validate" "1.x.x" -"@hapi/teamwork@5.x.x": +"@hapi/shot@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@hapi/shot/-/shot-5.0.5.tgz#a25c23d18973bec93c7969c51bf9579632a5bebd" + integrity sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A== + dependencies: + "@hapi/hoek" "9.x.x" + "@hapi/validate" "1.x.x" + +"@hapi/somever@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@hapi/somever/-/somever-3.0.1.tgz#9961cd5bdbeb5bb1edc0b2acdd0bb424066aadcc" + integrity sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w== + dependencies: + "@hapi/bounce" "2.x.x" + "@hapi/hoek" "9.x.x" + +"@hapi/statehood@^7.0.3": + version "7.0.4" + resolved "https://registry.yarnpkg.com/@hapi/statehood/-/statehood-7.0.4.tgz#6acb9d0817b5c657089356f7d9fd60af0bce4f41" + integrity sha512-Fia6atroOVmc5+2bNOxF6Zv9vpbNAjEXNcUbWXavDqhnJDlchwUUwKS5LCi5mGtCTxRhUKKHwuxuBZJkmLZ7fw== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/bounce" "2.x.x" + "@hapi/bourne" "2.x.x" + "@hapi/cryptiles" "5.x.x" + "@hapi/hoek" "9.x.x" + "@hapi/iron" "6.x.x" + "@hapi/validate" "1.x.x" + +"@hapi/subtext@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@hapi/subtext/-/subtext-7.1.0.tgz#b4d1ea2aeab1923ac130a24e75921e38fab5b15b" + integrity sha512-n94cU6hlvsNRIpXaROzBNEJGwxC+HA69q769pChzej84On8vsU14guHDub7Pphr/pqn5b93zV3IkMPDU5AUiXA== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/bourne" "2.x.x" + "@hapi/content" "^5.0.2" + "@hapi/file" "2.x.x" + "@hapi/hoek" "9.x.x" + "@hapi/pez" "^5.1.0" + "@hapi/wreck" "17.x.x" + +"@hapi/teamwork@5.x.x", "@hapi/teamwork@^5.1.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@hapi/teamwork/-/teamwork-5.1.1.tgz#4d2ba3cac19118a36c44bf49a3a47674de52e4e4" integrity sha512-1oPx9AE5TIv+V6Ih54RP9lTZBso3rP8j4Xhb6iSVwPXtAM+sDopl5TFMv5Paw73UnpZJ9gjcrTE1BXrWt9eQrg== @@ -3616,7 +3779,7 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@hapi/validate@1.x.x": +"@hapi/validate@1.x.x", "@hapi/validate@^1.1.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@hapi/validate/-/validate-1.1.3.tgz#f750a07283929e09b51aa16be34affb44e1931ad" integrity sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA== @@ -3624,6 +3787,22 @@ "@hapi/hoek" "^9.0.0" "@hapi/topo" "^5.0.0" +"@hapi/vise@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@hapi/vise/-/vise-4.0.0.tgz#c6a94fe121b94a53bf99e7489f7fcc74c104db02" + integrity sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg== + dependencies: + "@hapi/hoek" "9.x.x" + +"@hapi/wreck@17.x.x": + version "17.2.0" + resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-17.2.0.tgz#a5b69b724fa8fa25550fb02f55c649becfc59f63" + integrity sha512-pJ5kjYoRPYDv+eIuiLQqhGon341fr2bNIYZjuotuPJG/3Ilzr/XtI+JAp0A86E2bYfsS3zBPABuS2ICkaXFT8g== + dependencies: + "@hapi/boom" "9.x.x" + "@hapi/bourne" "2.x.x" + "@hapi/hoek" "9.x.x" + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -21706,7 +21885,7 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.52.0, mime-db@1.x.x, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== From 78c6a9dff4766e6f91b2220ab769f67a4e4c208d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 29 Jan 2024 15:57:12 +0100 Subject: [PATCH 24/39] test(e2e): Add Node SDK re-exports consistency e2e test (#10389) Introduce an e2e test that checks if all `@sentry/node` exports were re-exported in packages that depend on `@sentry/node`. It's not a real e2e test, as there's no actual test application, but we only execute a script that imports various SDKs depending on `@sentry/node`. However, IMO it makes a lot of sense to test the export of the final tarballs which we already have the infrastructure for in our e2e tests. see PR description for more details --- .github/workflows/build.yml | 1 + .../node-exports-test-app/.npmrc | 2 + .../node-exports-test-app/README.md | 18 +++ .../node-exports-test-app/package.json | 32 ++++++ .../scripts/consistentExports.ts | 103 ++++++++++++++++++ .../node-exports-test-app/tsconfig.json | 13 +++ 6 files changed, 169 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/node-exports-test-app/README.md create mode 100644 dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f24414286c8a..00459abbe701 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -913,6 +913,7 @@ jobs: 'generic-ts3.8', 'node-experimental-fastify-app', 'node-hapi-app', + 'node-exports-test-app', ] build-command: - false diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc b/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/README.md b/dev-packages/e2e-tests/test-applications/node-exports-test-app/README.md new file mode 100644 index 000000000000..8af36f6c6dad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/README.md @@ -0,0 +1,18 @@ +# Consistent Node Export Test + +This test "app" ensures that we consistently re-export exports from `@sentry/node` in packages depending on +`@sentry/node`. + +## How to add new package + +1. Add package as a dependency to the test app +2. In `scripts/consistentExports.ts`: + - add namespace import + - add `DEPENDENTS` entry + - add any ignores/exclusion entries as necessary + - if the package is still under development, you can also set `skip: true` + +## Limitations: + +- This script only checks top-level exports for now (e.g. `metrics` but no sub-exports like `metrics.increment`) +- This script only checks ESM transpiled code for now, not CJS diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json new file mode 100644 index 000000000000..056dd6836e61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json @@ -0,0 +1,32 @@ +{ + "name": "node-express-app", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "pnpm build && node dist/consistentExports.js", + "test": " node dist/consistentExports.js", + "clean": "npx rimraf node_modules,pnpm-lock.yaml,dist", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/node": "latest || *", + "@sentry/sveltekit": "latest || *", + "@sentry/remix": "latest || *", + "@sentry/astro": "latest || *", + "@sentry/nextjs": "latest || *", + "@sentry/serverless": "latest || *", + "@sentry/bun": "latest || *", + "@sentry/types": "latest || *", + "@types/node": "18.15.1", + "typescript": "4.9.5" + }, + "devDependencies": { + "ts-node": "10.9.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts new file mode 100644 index 000000000000..a488ef463412 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -0,0 +1,103 @@ +import * as SentryAstro from '@sentry/astro'; +// import * as SentryBun from '@sentry/bun'; +import * as SentryNextJs from '@sentry/nextjs'; +import * as SentryNode from '@sentry/node'; +import * as SentryRemix from '@sentry/remix'; +import * as SentryServerless from '@sentry/serverless'; +import * as SentrySvelteKit from '@sentry/sveltekit'; + +/* List of exports that are safe to ignore / we don't require in any depending package */ +const NODE_EXPORTS_IGNORE = [ + 'default', + // Probably generated by transpilation, no need to require it + '__esModule', + // this function was deprecates almost immediately after it was introduced + // due to a name change (startSpan). No need to re-export it IMHO. + 'startActiveSpan', + // this was never meant for external use (and documented as such) + 'trace', + // These Node exports were only made for type definition fixes (see #10339) + 'Undici', + 'Http', + 'DebugSession', + 'AnrIntegrationOptions', + 'LocalVariablesIntegrationOptions', + // deprecated + 'spanStatusfromHttpCode', +]; + +type Dependent = { + package: string; + exports: string[]; + ignoreExports?: string[]; + skip?: boolean; +}; + +const DEPENDENTS: Dependent[] = [ + { + package: '@sentry/astro', + exports: Object.keys(SentryAstro), + }, + { + package: '@sentry/nextjs', + // Next.js doesn't require explicit exports, so we can just merge top level and `default` exports: + // @ts-expect-error: `default` is not in the type definition but it's defined + exports: Object.keys({ ...SentryNextJs, ...SentryNextJs.default }), + ignoreExports: ['withSentryConfig'], + }, + { + package: '@sentry/remix', + exports: Object.keys(SentryRemix), + // TODO: Fix exports in remix + skip: true, + }, + { + package: '@sentry/serverless', + exports: Object.keys(SentryServerless), + ignoreExports: [ + // Deprecated, no need to add this now to serverless + 'extractTraceparentData', + 'getModuleFromFilename', + // TODO: Should these be exported from serverless? + 'cron', + 'enableAnrDetection', + 'runWithAsyncContext', + 'hapiErrorPlugin', + ], + // TODO: Fix exports in serverless + skip: true, + }, + { + package: '@sentry/sveltekit', + exports: Object.keys(SentrySvelteKit), + // TODO: Fix exports in sveltekit + skip: true, + }, +]; + +/* Sanitized list of node exports */ +const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e)); + +console.log('🔎 Checking for consistent exports of @sentry/node exports in depending packages'); + +const missingExports: Record = {}; +const dependentsToCheck = DEPENDENTS.filter(d => !d.skip); + +for (const nodeExport of nodeExports) { + for (const dependent of dependentsToCheck) { + if (dependent.ignoreExports?.includes(nodeExport)) { + continue; + } + if (!dependent.exports.includes(nodeExport)) { + missingExports[dependent.package] = [...(missingExports[dependent.package] ?? []), nodeExport]; + } + } +} + +if (Object.keys(missingExports).length > 0) { + console.error('\n❌ Found missing exports from @sentry/node in the following packages:\n'); + console.log(JSON.stringify(missingExports, null, 2)); + process.exit(1); +} + +console.log('✅ All good :)'); diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json new file mode 100644 index 000000000000..fc22710d69dc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["ES6"], + "strict": true, + "outDir": "dist", + "target": "ESNext", + "moduleResolution": "node", + "skipLibCheck": true + }, + "include": ["scripts/**/*.ts"] +} From 7dc92604e4a9448a17512d5622e28a3976b9267e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 29 Jan 2024 11:27:14 -0500 Subject: [PATCH 25/39] ref(tracing): Implement changes to new `browserTracingIntegration` (#10393) This implements changes to the new integration, based on feedback from the team. The main thing this addresses is the way we disable the default span creation. The original idea was to make this work with custom router instrumentations (e.g. for react router) that may be added as separate integrations. This required us to keep track of if another, non-default implementation has been added, to opt out of the defaults. With this change, we do not do this anymore. Instead, the defaults should be configured by wrapping integrations. Some examples for how this should then work: 1. Angular: ```ts export function browserTracingIntegration( options?: Parameters[0], ): Integration { // do not use default navigation, as we provide our own // actual code for this is in the angular service options.instrumentNavigation = false; return originalBrowserTracingIntegration(options); } ``` 2. Vue ```ts export function browserTracingIntegration( options?: Parameters[0] & { router?: VueRouter }, ): Integration { if (options.router) { options.instrumentNavigation = false; const integration = originalBrowserTracingIntegration(options); const originalSetupAfterAll = integration.setupAfterAll; integration.setupAfterAll = (client: Client) => { setupVueRoutingInstrumentation(client); // some method originalSetupAfterAll(client); } } return originalBrowserTracingIntegration(options); } ``` 3. React ```ts export function browserTracingIntegration( options?: Parameters[0] & { router?: ReactRouterInstrumentation }, ): Integration { if (options.router) { options.instrumentNavigation = false; options.instrumentPageLoad = false; const integration = originalBrowserTracingIntegration(options); const originalSetupAfterAll = integration.setupAfterAll; integration.setupAfterAll = (client: Client) => { // setup custom routing instrumentation options.router(client); // or whatever API we want there originalSetupAfterAll(client); } } return originalBrowserTracingIntegration(options); } ``` --- packages/browser/src/index.ts | 2 - .../src/browser/browserTracingIntegration.ts | 58 ++++--------------- .../tracing-internal/src/browser/index.ts | 2 - packages/tracing-internal/src/index.ts | 2 - 4 files changed, 11 insertions(+), 53 deletions(-) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 658dff61716b..408a64081a02 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -60,8 +60,6 @@ export { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, - disableDefaultBrowserTracingNavigationSpan, - disableDefaultBrowserTracingPageLoadSpan, } from '@sentry-internal/tracing'; export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; export { diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 59d15d9588ee..87169c55c8b5 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -74,12 +74,14 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { /** * If a span should be created on page load. + * If this is set to `false`, this integration will not start the default page load span. * Default: true */ instrumentPageLoad: boolean; /** * If a span should be created on navigation (history change). + * If this is set to `false`, this integration will not start the default navigation spans. * Default: true */ instrumentNavigation: boolean; @@ -143,9 +145,6 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { ...defaultRequestInstrumentationOptions, }; -let shouldUseDefaultPageLoadSpan = true; -let shouldUseDefaultNavigationSpan = true; - /** * The Browser Tracing integration automatically instruments browser pageload/navigation * actions as transactions, and captures requests, metrics and errors as spans. @@ -308,13 +307,6 @@ export const _browserTracingIntegration = ((_options: Partial { - // We check this inside of the hook handler, so that if a custom instrumentation triggers this, - // we don't need to check this option in the instrumentation, but can simply invoke it - // without needing to know the options of this integration - if (!options.instrumentNavigation) { - return; - } - if (activeSpan) { DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); // If there's an open transaction on the scope, we need to finish it before creating an new one. @@ -324,13 +316,6 @@ export const _browserTracingIntegration = ((_options: Partial { - // We check this inside of the hook handler, so that if a custom instrumentation triggers this, - // we don't need to check this option in the instrumentation, but can simply invoke it - // without needing to know the options of this integration - if (!options.instrumentPageLoad) { - return; - } - if (activeSpan) { DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`); // If there's an open transaction on the scope, we need to finish it before creating an new one. @@ -340,7 +325,7 @@ export const _browserTracingIntegration = ((_options: Partial Date: Mon, 29 Jan 2024 15:33:04 -0500 Subject: [PATCH 26/39] fix(tracing): Only create request span if there is active span (#10375) This was a regression introduced with https://github.com/getsentry/sentry-javascript/pull/10236, we shouldn't arbitrarily call `startInactiveSpan` given we create transactions under the hood currently. --- .../tracing-internal/src/browser/request.ts | 28 +++++++++++-------- packages/tracing-internal/src/common/fetch.ts | 28 +++++++++++-------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 557256774f73..89d5034a5425 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -279,18 +280,21 @@ export function xhrCallback( const scope = getCurrentScope(); const isolationScope = getIsolationScope(); - const span = shouldCreateSpanResult - ? startInactiveSpan({ - attributes: { - type: 'xhr', - 'http.method': sentryXhrData.method, - url: sentryXhrData.url, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', - }, - name: `${sentryXhrData.method} ${sentryXhrData.url}`, - op: 'http.client', - }) - : undefined; + // only create a child span if there is an active span. This is because + // `startInactiveSpan` can still create a transaction under the hood + const span = + shouldCreateSpanResult && getActiveSpan() + ? startInactiveSpan({ + attributes: { + type: 'xhr', + 'http.method': sentryXhrData.method, + url: sentryXhrData.url, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', + }, + name: `${sentryXhrData.method} ${sentryXhrData.url}`, + op: 'http.client', + }) + : undefined; if (span) { xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 3d973b0055b9..2177d1939987 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,5 +1,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -81,18 +82,21 @@ export function instrumentFetchRequest( const { method, url } = handlerData.fetchData; - const span = shouldCreateSpanResult - ? startInactiveSpan({ - attributes: { - url, - type: 'fetch', - 'http.method': method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, - }, - name: `${method} ${url}`, - op: 'http.client', - }) - : undefined; + // only create a child span if there is an active span. This is because + // `startInactiveSpan` can still create a transaction under the hood + const span = + shouldCreateSpanResult && getActiveSpan() + ? startInactiveSpan({ + attributes: { + url, + type: 'fetch', + 'http.method': method, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + }, + name: `${method} ${url}`, + op: 'http.client', + }) + : undefined; if (span) { handlerData.fetchData.__span = span.spanContext().spanId; From ea85419c957059ec4bfeda9ce401cbacb0235be4 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 29 Jan 2024 17:11:44 -0500 Subject: [PATCH 27/39] ref(serverless): Convert integrations to functional approach (#10329) This PR updates the serverless integrations to use the functional approach, and rewrites all of the serverless unit tests. In the unit test rewrite we remove the usage of global mocks and instead do per file mocks. I dislike the amount of mocks that still exist, but to prevent any regressions I only tried to translate the test code. --- packages/serverless/src/awslambda.ts | 9 +- packages/serverless/src/awsservices.ts | 104 +++++---- packages/serverless/src/gcpfunction/index.ts | 12 +- packages/serverless/src/google-cloud-grpc.ts | 88 ++++---- packages/serverless/src/google-cloud-http.ts | 92 ++++---- packages/serverless/src/index.ts | 3 +- .../serverless/test/__mocks__/@sentry/node.ts | 54 ----- packages/serverless/test/awslambda.test.ts | 203 ++++++++++------- packages/serverless/test/awsservices.test.ts | 82 +++++-- packages/serverless/test/gcpfunction.test.ts | 205 ++++++++++-------- .../serverless/test/google-cloud-grpc.test.ts | 44 +++- .../serverless/test/google-cloud-http.test.ts | 41 +++- 12 files changed, 541 insertions(+), 396 deletions(-) delete mode 100644 packages/serverless/test/__mocks__/@sentry/node.ts diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 89086e7a1c77..10f0e0e29c81 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -2,7 +2,6 @@ import { existsSync } from 'fs'; import { hostname } from 'os'; import { basename, resolve } from 'path'; import { types } from 'util'; -/* eslint-disable max-lines */ import type { NodeOptions, Scope } from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import { @@ -19,12 +18,12 @@ import { } from '@sentry/node'; import type { Integration, Options, SdkMetadata, Span } from '@sentry/types'; import { isString, logger } from '@sentry/utils'; -// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil import type { Context, Handler } from 'aws-lambda'; import { performance } from 'perf_hooks'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import { AWSServices } from './awsservices'; +import { awsServicesIntegration } from './awsservices'; + import { DEBUG_BUILD } from './debug-build'; import { markEventUnhandled } from './utils'; @@ -71,12 +70,12 @@ export interface WrapperOptions { export const defaultIntegrations: Integration[] = [ // eslint-disable-next-line deprecation/deprecation ...nodeDefaultIntegrations, - new AWSServices({ optional: true }), + awsServicesIntegration({ optional: true }), ]; /** Get the default integrations for the AWSLambda SDK. */ export function getDefaultIntegrations(options: Options): Integration[] { - return [...getNodeDefaultIntegrations(options), new AWSServices({ optional: true })]; + return [...getNodeDefaultIntegrations(options), awsServicesIntegration({ optional: true })]; } interface AWSLambdaOptions extends NodeOptions { diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 36a789c52632..84e6ec93ff25 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -1,6 +1,6 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { startInactiveSpan } from '@sentry/node'; -import type { Integration, Span } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; +import { getClient, startInactiveSpan } from '@sentry/node'; +import type { Client, Integration, IntegrationClass, IntegrationFn, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; // 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. @@ -16,63 +16,73 @@ interface AWSService { serviceIdentifier: string; } -/** AWS service requests tracking */ -export class AWSServices implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'AWSServices'; +const INTEGRATION_NAME = 'AWSServices'; - /** - * @inheritDoc - */ - public name: string; +const SETUP_CLIENTS = new WeakMap(); - private readonly _optional: boolean; +const _awsServicesIntegration = ((options: { optional?: boolean } = {}) => { + const optional = options.optional || false; + return { + name: INTEGRATION_NAME, + setupOnce() { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const awsModule = require('aws-sdk/global') as typeof AWS; + fill(awsModule.Service.prototype, 'makeRequest', wrapMakeRequest); + } catch (e) { + if (!optional) { + throw e; + } + } + }, + setup(client) { + SETUP_CLIENTS.set(client, true); + }, + }; +}) satisfies IntegrationFn; - public constructor(options: { optional?: boolean } = {}) { - this.name = AWSServices.id; +export const awsServicesIntegration = defineIntegration(_awsServicesIntegration); - this._optional = options.optional || false; - } +/** + * AWS Service Request Tracking. + * + * @deprecated Use `awsServicesIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const AWSServices = convertIntegrationFnToClass( + INTEGRATION_NAME, + awsServicesIntegration, +) as IntegrationClass; - /** - * @inheritDoc - */ - public setupOnce(): void { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const awsModule = require('aws-sdk/global') as typeof AWS; - fill(awsModule.Service.prototype, 'makeRequest', wrapMakeRequest); - } catch (e) { - if (!this._optional) { - throw e; - } - } - } -} +// eslint-disable-next-line deprecation/deprecation +export type AWSServices = typeof AWSServices; -/** */ +/** + * Patches AWS SDK request to create `http.client` spans. + */ function wrapMakeRequest( orig: MakeRequestFunction, ): MakeRequestFunction { return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { let span: Span | undefined; const req = orig.call(this, operation, params); - req.on('afterBuild', () => { - span = startInactiveSpan({ - name: describe(this, operation, params), - op: 'http.client', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', - }, + + if (SETUP_CLIENTS.has(getClient() as Client)) { + req.on('afterBuild', () => { + span = startInactiveSpan({ + name: describe(this, operation, params), + op: 'http.client', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, + }); }); - }); - req.on('complete', () => { - if (span) { - span.end(); - } - }); + req.on('complete', () => { + if (span) { + span.end(); + } + }); + } if (callback) { req.send(callback); diff --git a/packages/serverless/src/gcpfunction/index.ts b/packages/serverless/src/gcpfunction/index.ts index 3907e84aabc8..50556634c5fc 100644 --- a/packages/serverless/src/gcpfunction/index.ts +++ b/packages/serverless/src/gcpfunction/index.ts @@ -7,8 +7,8 @@ import { } from '@sentry/node'; import type { Integration, Options, SdkMetadata } from '@sentry/types'; -import { GoogleCloudGrpc } from '../google-cloud-grpc'; -import { GoogleCloudHttp } from '../google-cloud-http'; +import { googleCloudGrpcIntegration } from '../google-cloud-grpc'; +import { googleCloudHttpIntegration } from '../google-cloud-http'; export * from './http'; export * from './events'; @@ -18,16 +18,16 @@ export * from './cloud_events'; export const defaultIntegrations: Integration[] = [ // eslint-disable-next-line deprecation/deprecation ...defaultNodeIntegrations, - new GoogleCloudHttp({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing. - new GoogleCloudGrpc({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing. + googleCloudHttpIntegration({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing. + googleCloudGrpcIntegration({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing. ]; /** Get the default integrations for the GCP SDK. */ export function getDefaultIntegrations(options: Options): Integration[] { return [ ...getDefaultNodeIntegrations(options), - new GoogleCloudHttp({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing. - new GoogleCloudGrpc({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing. + googleCloudHttpIntegration({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing. + googleCloudGrpcIntegration({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing. ]; } diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index ba2ddb038a03..1423e2b3ad5a 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -1,7 +1,12 @@ import type { EventEmitter } from 'events'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + convertIntegrationFnToClass, + defineIntegration, + getClient, +} from '@sentry/core'; import { startInactiveSpan } from '@sentry/node'; -import type { Integration } from '@sentry/types'; +import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { fill } from '@sentry/utils'; interface GrpcFunction extends CallableFunction { @@ -26,45 +31,52 @@ interface Stub { [key: string]: GrpcFunctionObject; } -/** Google Cloud Platform service requests tracking for GRPC APIs */ -export class GoogleCloudGrpc implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'GoogleCloudGrpc'; +const SERVICE_PATH_REGEX = /^(\w+)\.googleapis.com$/; - /** - * @inheritDoc - */ - public name: string; +const INTEGRATION_NAME = 'GoogleCloudGrpc'; - private readonly _optional: boolean; +const SETUP_CLIENTS = new WeakMap(); - public constructor(options: { optional?: boolean } = {}) { - this.name = GoogleCloudGrpc.id; +const _googleCloudGrpcIntegration = ((options: { optional?: boolean } = {}) => { + const optional = options.optional || false; + return { + name: INTEGRATION_NAME, + setupOnce() { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const gaxModule = require('google-gax'); + fill( + gaxModule.GrpcClient.prototype, // eslint-disable-line @typescript-eslint/no-unsafe-member-access + 'createStub', + wrapCreateStub, + ); + } catch (e) { + if (!optional) { + throw e; + } + } + }, + setup(client) { + SETUP_CLIENTS.set(client, true); + }, + }; +}) satisfies IntegrationFn; - this._optional = options.optional || false; - } +export const googleCloudGrpcIntegration = defineIntegration(_googleCloudGrpcIntegration); - /** - * @inheritDoc - */ - public setupOnce(): void { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const gaxModule = require('google-gax'); - fill( - gaxModule.GrpcClient.prototype, // eslint-disable-line @typescript-eslint/no-unsafe-member-access - 'createStub', - wrapCreateStub, - ); - } catch (e) { - if (!this._optional) { - throw e; - } - } - } -} +/** + * Google Cloud Platform service requests tracking for GRPC APIs. + * + * @deprecated Use `googleCloudGrpcIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const GoogleCloudGrpc = convertIntegrationFnToClass( + INTEGRATION_NAME, + googleCloudGrpcIntegration, +) as IntegrationClass; + +// eslint-disable-next-line deprecation/deprecation +export type GoogleCloudGrpc = typeof GoogleCloudGrpc; /** Returns a wrapped function that returns a stub with tracing enabled */ function wrapCreateStub(origCreate: CreateStubFunc): CreateStubFunc { @@ -105,7 +117,7 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str (orig: GrpcFunction): GrpcFunction => (...args) => { const ret = orig.apply(stub, args); - if (typeof ret?.on !== 'function') { + if (typeof ret?.on !== 'function' || !SETUP_CLIENTS.has(getClient() as Client)) { return ret; } const span = startInactiveSpan({ @@ -127,6 +139,6 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str /** Identifies service by its address */ function identifyService(servicePath: string): string { - const match = servicePath.match(/^(\w+)\.googleapis.com$/); + const match = servicePath.match(SERVICE_PATH_REGEX); return match ? match[1] : servicePath; } diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 769519013785..ee300e78a010 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -1,9 +1,12 @@ -// '@google-cloud/common' import is expected to be type-only so it's erased in the final .js file. -// When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import type * as common from '@google-cloud/common'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + convertIntegrationFnToClass, + defineIntegration, + getClient, +} from '@sentry/core'; import { startInactiveSpan } from '@sentry/node'; -import type { Integration } from '@sentry/types'; +import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { fill } from '@sentry/utils'; type RequestOptions = common.DecorateRequestOptions; @@ -13,53 +16,60 @@ interface RequestFunction extends CallableFunction { (reqOpts: RequestOptions, callback: ResponseCallback): void; } -/** Google Cloud Platform service requests tracking for RESTful APIs */ -export class GoogleCloudHttp implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'GoogleCloudHttp'; +const INTEGRATION_NAME = 'GoogleCloudHttp'; - /** - * @inheritDoc - */ - public name: string; +const SETUP_CLIENTS = new WeakMap(); - private readonly _optional: boolean; +const _googleCloudHttpIntegration = ((options: { optional?: boolean } = {}) => { + const optional = options.optional || false; + return { + name: INTEGRATION_NAME, + setupOnce() { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const commonModule = require('@google-cloud/common') as typeof common; + fill(commonModule.Service.prototype, 'request', wrapRequestFunction); + } catch (e) { + if (!optional) { + throw e; + } + } + }, + setup(client) { + SETUP_CLIENTS.set(client, true); + }, + }; +}) satisfies IntegrationFn; - public constructor(options: { optional?: boolean } = {}) { - this.name = GoogleCloudHttp.id; +export const googleCloudHttpIntegration = defineIntegration(_googleCloudHttpIntegration); - this._optional = options.optional || false; - } +/** + * Google Cloud Platform service requests tracking for RESTful APIs. + * + * @deprecated Use `googleCloudHttpIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const GoogleCloudHttp = convertIntegrationFnToClass( + INTEGRATION_NAME, + googleCloudHttpIntegration, +) as IntegrationClass; - /** - * @inheritDoc - */ - public setupOnce(): void { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const commonModule = require('@google-cloud/common') as typeof common; - fill(commonModule.Service.prototype, 'request', wrapRequestFunction); - } catch (e) { - if (!this._optional) { - throw e; - } - } - } -} +// eslint-disable-next-line deprecation/deprecation +export type GoogleCloudHttp = typeof GoogleCloudHttp; /** Returns a wrapped function that makes a request with tracing enabled */ function wrapRequestFunction(orig: RequestFunction): RequestFunction { return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void { const httpMethod = reqOpts.method || 'GET'; - const span = startInactiveSpan({ - name: `${httpMethod} ${reqOpts.uri}`, - op: `http.client.${identifyService(this.apiEndpoint)}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', - }, - }); + const span = SETUP_CLIENTS.has(getClient() as Client) + ? startInactiveSpan({ + name: `${httpMethod} ${reqOpts.uri}`, + op: `http.client.${identifyService(this.apiEndpoint)}`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + }, + }) + : undefined; orig.call(this, reqOpts, (...args: Parameters) => { if (span) { span.end(); diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 3fef9fb94283..8db1e4ba5be0 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -3,7 +3,8 @@ import * as AWSLambda from './awslambda'; import * as GCPFunction from './gcpfunction'; export { AWSLambda, GCPFunction }; -export { AWSServices } from './awsservices'; +// eslint-disable-next-line deprecation/deprecation +export { AWSServices, awsServicesIntegration } from './awsservices'; // TODO(v8): We have to explicitly export these because of the namespace exports // above. This is because just doing `export * from '@sentry/node'` will not diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts deleted file mode 100644 index 5181b8a0a535..000000000000 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ /dev/null @@ -1,54 +0,0 @@ -const origSentry = jest.requireActual('@sentry/node'); -export const defaultIntegrations = origSentry.defaultIntegrations; // eslint-disable-line @typescript-eslint/no-unsafe-member-access -export const getDefaultIntegrations = origSentry.getDefaultIntegrations; // eslint-disable-line @typescript-eslint/no-unsafe-member-access -export const Handlers = origSentry.Handlers; // eslint-disable-line @typescript-eslint/no-unsafe-member-access -export const Integrations = origSentry.Integrations; -export const addRequestDataToEvent = origSentry.addRequestDataToEvent; -export const SDK_VERSION = '6.6.6'; -export const Severity = { - Warning: 'warning', -}; -export const continueTrace = origSentry.continueTrace; - -export const fakeScope = { - addEventProcessor: jest.fn(), - setTag: jest.fn(), - setContext: jest.fn(), - setSpan: jest.fn(), - setSDKProcessingMetadata: jest.fn(), - setPropagationContext: jest.fn(), -}; -export const fakeSpan = { - end: jest.fn(), - setHttpStatus: jest.fn(), -}; -export const init = jest.fn(); -export const addGlobalEventProcessor = jest.fn(); -export const getCurrentScope = jest.fn(() => fakeScope); -export const captureException = jest.fn(); -export const captureMessage = jest.fn(); -export const withScope = jest.fn(cb => cb(fakeScope)); -export const flush = jest.fn(() => Promise.resolve()); -export const getClient = jest.fn(() => ({})); -export const startSpanManual = jest.fn((ctx, callback: (span: any) => any) => callback(fakeSpan)); -export const startInactiveSpan = jest.fn(() => fakeSpan); - -export const resetMocks = (): void => { - fakeSpan.end.mockClear(); - fakeSpan.setHttpStatus.mockClear(); - - fakeScope.addEventProcessor.mockClear(); - fakeScope.setTag.mockClear(); - fakeScope.setContext.mockClear(); - fakeScope.setSpan.mockClear(); - - init.mockClear(); - addGlobalEventProcessor.mockClear(); - - captureException.mockClear(); - captureMessage.mockClear(); - withScope.mockClear(); - flush.mockClear(); - getClient.mockClear(); - startSpanManual.mockClear(); -}; diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index de851cda8bbb..57feede5a102 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,12 +1,59 @@ -// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import * as SentryNode from '@sentry/node'; + import type { Event } from '@sentry/types'; import type { Callback, Handler } from 'aws-lambda'; -import * as Sentry from '../src'; +import { init, wrapHandler } from '../src/awslambda'; + +const mockSpanEnd = jest.fn(); +const mockStartInactiveSpan = jest.fn((...spanArgs) => ({ ...spanArgs })); +const mockStartSpanManual = jest.fn((...spanArgs) => ({ ...spanArgs })); +const mockFlush = jest.fn((...args) => Promise.resolve(args)); +const mockWithScope = jest.fn(); +const mockCaptureMessage = jest.fn(); +const mockCaptureException = jest.fn(); +const mockInit = jest.fn(); + +const mockScope = { + setTag: jest.fn(), + setContext: jest.fn(), + addEventProcessor: jest.fn(), +}; -const { wrapHandler } = Sentry.AWSLambda; +jest.mock('@sentry/node', () => { + const original = jest.requireActual('@sentry/node'); + return { + ...original, + init: (options: unknown) => { + mockInit(options); + }, + startInactiveSpan: (...args: unknown[]) => { + mockStartInactiveSpan(...args); + return { end: mockSpanEnd }; + }, + startSpanManual: (...args: unknown[]) => { + mockStartSpanManual(...args); + mockSpanEnd(); + return original.startSpanManual(...args); + }, + getCurrentScope: () => { + return mockScope; + }, + flush: (...args: unknown[]) => { + return mockFlush(...args); + }, + withScope: (fn: (scope: unknown) => void) => { + mockWithScope(fn); + fn(mockScope); + }, + captureMessage: (...args: unknown[]) => { + mockCaptureMessage(...args); + }, + captureException: (...args: unknown[]) => { + mockCaptureException(...args); + }, + }; +}); // Default `timeoutWarningLimit` is 500ms so leaving some space for it to trigger when necessary const DEFAULT_EXECUTION_TIME = 100; @@ -34,21 +81,18 @@ const fakeCallback: Callback = (err, result) => { }; function expectScopeSettings() { - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(1); + expect(mockScope.addEventProcessor).toBeCalledTimes(1); // Test than an event processor to add `transaction` is registered for the scope - // @ts-expect-error see "Why @ts-expect-error" note - const eventProcessor = SentryNode.fakeScope.addEventProcessor.mock.calls[0][0]; + const eventProcessor = mockScope.addEventProcessor.mock.calls[0][0]; const event: Event = {}; eventProcessor(event); expect(event).toEqual({ transaction: 'functionName' }); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTag).toBeCalledWith('url', 'awslambda:///functionName'); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setContext).toBeCalledWith( + expect(mockScope.setTag).toBeCalledWith('server_name', expect.anything()); + + expect(mockScope.setTag).toBeCalledWith('url', 'awslambda:///functionName'); + + expect(mockScope.setContext).toBeCalledWith( 'aws.lambda', expect.objectContaining({ aws_request_id: 'awsRequestId', @@ -58,8 +102,8 @@ function expectScopeSettings() { remaining_time_in_millis: 100, }), ); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setContext).toBeCalledWith( + + expect(mockScope.setContext).toBeCalledWith( 'aws.cloudwatch.logs', expect.objectContaining({ log_group: 'logGroupName', @@ -73,11 +117,8 @@ describe('AWSLambda', () => { fakeEvent = { fortySix: 'o_O', }; - }); - afterEach(() => { - // @ts-expect-error see "Why @ts-expect-error" note - SentryNode.resetMocks(); + jest.clearAllMocks(); }); describe('wrapHandler() options', () => { @@ -88,7 +129,7 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(handler, { flushTimeout: 1337 }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(SentryNode.flush).toBeCalledWith(1337); + expect(mockFlush).toBeCalledWith(1337); }); test('captureTimeoutWarning enabled (default)', async () => { @@ -100,10 +141,9 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(handler); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(Sentry.withScope).toBeCalledTimes(1); - expect(Sentry.captureMessage).toBeCalled(); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTag).toBeCalledWith('timeout', '1s'); + expect(mockWithScope).toBeCalledTimes(1); + expect(mockCaptureMessage).toBeCalled(); + expect(mockScope.setTag).toBeCalledWith('timeout', '1s'); }); test('captureTimeoutWarning disabled', async () => { @@ -117,10 +157,9 @@ describe('AWSLambda', () => { }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(Sentry.withScope).toBeCalledTimes(0); - expect(Sentry.captureMessage).not.toBeCalled(); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTag).not.toBeCalledWith('timeout', '1s'); + expect(mockWithScope).toBeCalledTimes(0); + expect(mockCaptureMessage).not.toBeCalled(); + expect(mockScope.setTag).not.toBeCalledWith('timeout', '1s'); }); test('captureTimeoutWarning with configured timeoutWarningLimit', async () => { @@ -149,16 +188,15 @@ describe('AWSLambda', () => { fakeCallback, ); - expect(Sentry.captureMessage).toBeCalled(); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTag).toBeCalledWith('timeout', '1m40s'); + expect(mockCaptureMessage).toBeCalled(); + expect(mockScope.setTag).toBeCalledWith('timeout', '1m40s'); }); test('captureAllSettledReasons disabled (default)', async () => { const handler = () => Promise.resolve([{ status: 'rejected', reason: new Error() }]); const wrappedHandler = wrapHandler(handler, { flushTimeout: 1337 }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(SentryNode.captureException).toBeCalledTimes(0); + expect(mockCaptureException).toBeCalledTimes(0); }); test('captureAllSettledReasons enable', async () => { @@ -172,9 +210,9 @@ describe('AWSLambda', () => { ]); const wrappedHandler = wrapHandler(handler, { flushTimeout: 1337, captureAllSettledReasons: true }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(SentryNode.captureException).toHaveBeenNthCalledWith(1, error, expect.any(Function)); - expect(SentryNode.captureException).toHaveBeenNthCalledWith(2, error2, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledTimes(2); + expect(mockCaptureException).toHaveBeenNthCalledWith(1, error, expect.any(Function)); + expect(mockCaptureException).toHaveBeenNthCalledWith(2, error2, expect.any(Function)); + expect(mockCaptureException).toBeCalledTimes(2); }); // "wrapHandler() ... successful execution" tests the default of startTrace enabled @@ -185,11 +223,10 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(handler, { startTrace: false }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(0); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTag).toBeCalledTimes(0); - expect(SentryNode.startSpanManual).toBeCalledTimes(0); + expect(mockScope.addEventProcessor).toBeCalledTimes(0); + + expect(mockScope.setTag).toBeCalledTimes(0); + expect(mockStartSpanManual).toBeCalledTimes(0); }); }); @@ -214,11 +251,11 @@ describe('AWSLambda', () => { }; expect(rv).toStrictEqual(42); - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); expectScopeSettings(); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + + expect(mockSpanEnd).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); }); test('unsuccessful execution', async () => { @@ -243,12 +280,12 @@ describe('AWSLambda', () => { metadata: {}, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); expectScopeSettings(); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + + expect(mockSpanEnd).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); } }); @@ -273,7 +310,7 @@ describe('AWSLambda', () => { }; const handler: Handler = (_event, _context, callback) => { - expect(SentryNode.startSpanManual).toBeCalledWith( + expect(mockStartSpanManual).toBeCalledWith( expect.objectContaining({ parentSpanId: '1121201211212012', parentSampled: false, @@ -326,12 +363,12 @@ describe('AWSLambda', () => { metadata: { dynamicSamplingContext: {} }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); expectScopeSettings(); - expect(SentryNode.captureException).toBeCalledWith(e, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockCaptureException).toBeCalledWith(e, expect.any(Function)); + + expect(mockSpanEnd).toBeCalled(); + expect(mockFlush).toBeCalled(); } }); }); @@ -357,11 +394,11 @@ describe('AWSLambda', () => { }; expect(rv).toStrictEqual(42); - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); expectScopeSettings(); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + + expect(mockSpanEnd).toBeCalled(); + expect(mockFlush).toBeCalled(); }); test('event and context are correctly passed to the original handler', async () => { @@ -397,12 +434,12 @@ describe('AWSLambda', () => { metadata: {}, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); expectScopeSettings(); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + + expect(mockSpanEnd).toBeCalled(); + expect(mockFlush).toBeCalled(); } }); @@ -414,9 +451,7 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(handler); - jest.spyOn(Sentry, 'flush').mockImplementationOnce(async () => { - throw new Error(); - }); + mockFlush.mockImplementationOnce(() => Promise.reject(new Error('wat'))); await expect(wrappedHandler(fakeEvent, fakeContext, fakeCallback)).resolves.toBe('some string'); }); @@ -443,11 +478,11 @@ describe('AWSLambda', () => { }; expect(rv).toStrictEqual(42); - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); expectScopeSettings(); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + + expect(mockSpanEnd).toBeCalled(); + expect(mockFlush).toBeCalled(); }); test('event and context are correctly passed to the original handler', async () => { @@ -483,12 +518,12 @@ describe('AWSLambda', () => { metadata: {}, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); expectScopeSettings(); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + + expect(mockSpanEnd).toBeCalled(); + expect(mockFlush).toBeCalled(); } }); }); @@ -505,9 +540,9 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - const scopeFunction = SentryNode.captureException.mock.calls[0][1]; + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + + const scopeFunction = mockCaptureException.mock.calls[0][1]; const event: Event = { exception: { values: [{}] } }; let evtProcessor: ((e: Event) => Event) | undefined = undefined; scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) }); @@ -523,9 +558,9 @@ describe('AWSLambda', () => { describe('init()', () => { test('calls Sentry.init with correct sdk info metadata', () => { - Sentry.AWSLambda.init({}); + init({}); - expect(Sentry.init).toBeCalledWith( + expect(mockInit).toBeCalledWith( expect.objectContaining({ _metadata: { sdk: { @@ -534,10 +569,10 @@ describe('AWSLambda', () => { packages: [ { name: 'npm:@sentry/serverless', - version: '6.6.6', + version: expect.any(String), }, ], - version: '6.6.6', + version: expect.any(String), }, }, }), diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts index b18b1d8dd9af..bbfc3240c3ac 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/serverless/test/awsservices.test.ts @@ -1,22 +1,53 @@ -import * as SentryNode from '@sentry/node'; +import { NodeClient, createTransport, setCurrentClient } from '@sentry/node'; import * as AWS from 'aws-sdk'; import * as nock from 'nock'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { AWSServices } from '../src/awsservices'; +import { awsServicesIntegration } from '../src/awsservices'; -describe('AWSServices', () => { - beforeAll(() => { - new AWSServices().setupOnce(); +const mockSpanEnd = jest.fn(); +const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs })); + +jest.mock('@sentry/node', () => { + return { + ...jest.requireActual('@sentry/node'), + startInactiveSpan: (ctx: unknown) => { + mockStartInactiveSpan(ctx); + return { end: mockSpanEnd }; + }, + }; +}); + +describe('awsServicesIntegration', () => { + const mockClient = new NodeClient({ + tracesSampleRate: 1.0, + integrations: [], + dsn: 'https://withAWSServices@domain/123', + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], }); - afterEach(() => { - // @ts-expect-error see "Why @ts-expect-error" note - SentryNode.resetMocks(); + + const integration = awsServicesIntegration(); + mockClient.addIntegration(integration); + + const mockClientWithoutIntegration = new NodeClient({ + tracesSampleRate: 1.0, + integrations: [], + dsn: 'https://withoutAWSServices@domain/123', + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], }); + afterAll(() => { nock.restore(); }); + beforeEach(() => { + setCurrentClient(mockClient); + mockSpanEnd.mockClear(); + mockStartInactiveSpan.mockClear(); + }); + describe('S3 tracing', () => { const s3 = new AWS.S3({ accessKeyId: '-', secretAccessKey: '-' }); @@ -24,15 +55,22 @@ describe('AWSServices', () => { nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents'); const data = await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise(); expect(data.Body?.toString('utf-8')).toEqual('contents'); - expect(SentryNode.startInactiveSpan).toBeCalledWith({ + expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, name: 'aws.s3.getObject foo', }); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); + + expect(mockSpanEnd).toHaveBeenCalledTimes(1); + }); + + test('getObject with integration-less client', async () => { + setCurrentClient(mockClientWithoutIntegration); + nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents'); + await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise(); + expect(mockStartInactiveSpan).not.toBeCalled(); }); test('getObject with callback', done => { @@ -43,7 +81,7 @@ describe('AWSServices', () => { expect(data.Body?.toString('utf-8')).toEqual('contents'); done(); }); - expect(SentryNode.startInactiveSpan).toBeCalledWith({ + expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client', name: 'aws.s3.getObject foo', attributes: { @@ -51,6 +89,16 @@ describe('AWSServices', () => { }, }); }); + + test('getObject with callback with integration-less client', done => { + setCurrentClient(mockClientWithoutIntegration); + expect.assertions(1); + nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents'); + s3.getObject({ Bucket: 'foo', Key: 'bar' }, () => { + done(); + }); + expect(mockStartInactiveSpan).not.toBeCalled(); + }); }); describe('Lambda', () => { @@ -60,13 +108,21 @@ describe('AWSServices', () => { nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply'); const data = await lambda.invoke({ FunctionName: 'foo' }).promise(); expect(data.Payload?.toString('utf-8')).toEqual('reply'); - expect(SentryNode.startInactiveSpan).toBeCalledWith({ + expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, name: 'aws.lambda.invoke foo', }); + expect(mockSpanEnd).toHaveBeenCalledTimes(1); + }); + + test('invoke with integration-less client', async () => { + setCurrentClient(mockClientWithoutIntegration); + nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply'); + await lambda.invoke({ FunctionName: 'foo' }).promise(); + expect(mockStartInactiveSpan).not.toBeCalled(); }); }); }); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index b9794553961f..1fc58c37fdce 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -1,11 +1,9 @@ import * as domain from 'domain'; -import * as SentryCore from '@sentry/core'; -import * as SentryNode from '@sentry/node'; import type { Event, Integration } from '@sentry/types'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import * as Sentry from '../src'; + import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction'; import type { CloudEventFunction, @@ -17,12 +15,65 @@ import type { Response, } from '../src/gcpfunction/general'; -describe('GCPFunction', () => { - const setHttpStatusSpy = jest.spyOn(SentryCore, 'setHttpStatus').mockImplementation(() => {}); +import { init } from '../src/gcpfunction'; + +const mockStartInactiveSpan = jest.fn((...spanArgs) => ({ ...spanArgs })); +const mockStartSpanManual = jest.fn((...spanArgs) => ({ ...spanArgs })); +const mockFlush = jest.fn((...args) => Promise.resolve(args)); +const mockWithScope = jest.fn(); +const mockCaptureMessage = jest.fn(); +const mockCaptureException = jest.fn(); +const mockInit = jest.fn(); + +const mockScope = { + setTag: jest.fn(), + setContext: jest.fn(), + addEventProcessor: jest.fn(), + setSDKProcessingMetadata: jest.fn(), +}; + +const mockSpan = { + end: jest.fn(), +}; + +jest.mock('@sentry/node', () => { + const original = jest.requireActual('@sentry/node'); + return { + ...original, + init: (options: unknown) => { + mockInit(options); + }, + startInactiveSpan: (...args: unknown[]) => { + mockStartInactiveSpan(...args); + return mockSpan; + }, + startSpanManual: (...args: unknown[]) => { + mockStartSpanManual(...args); + mockSpan.end(); + return original.startSpanManual(...args); + }, + getCurrentScope: () => { + return mockScope; + }, + flush: (...args: unknown[]) => { + return mockFlush(...args); + }, + withScope: (fn: (scope: unknown) => void) => { + mockWithScope(fn); + fn(mockScope); + }, + captureMessage: (...args: unknown[]) => { + mockCaptureMessage(...args); + }, + captureException: (...args: unknown[]) => { + mockCaptureException(...args); + }, + }; +}); - afterEach(() => { - // @ts-expect-error see "Why @ts-expect-error" note - SentryNode.resetMocks(); +describe('GCPFunction', () => { + beforeEach(() => { + jest.clearAllMocks(); }); async function handleHttp(fn: HttpFunction, trace_headers: { [key: string]: string } | null = null): Promise { @@ -93,7 +144,7 @@ describe('GCPFunction', () => { const wrappedHandler = wrapHttpFunction(handler, { flushTimeout: 1337 }); await handleHttp(wrappedHandler); - expect(SentryNode.flush).toBeCalledWith(1337); + expect(mockFlush).toBeCalledWith(1337); }); }); @@ -116,12 +167,9 @@ describe('GCPFunction', () => { metadata: {}, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(setHttpStatusSpy).toBeCalledWith(SentryNode.fakeSpan, 200); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); }); test('incoming trace headers are correctly parsed and used', async () => { @@ -153,7 +201,7 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); }); test('capture error', async () => { @@ -182,11 +230,10 @@ describe('GCPFunction', () => { metadata: { dynamicSamplingContext: {} }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalled(); }); test('should not throw when flush rejects', async () => { @@ -207,7 +254,7 @@ describe('GCPFunction', () => { const mockEnd = jest.fn(); const response = { end: mockEnd } as unknown as Response; - jest.spyOn(Sentry, 'flush').mockImplementationOnce(async () => { + mockFlush.mockImplementationOnce(async () => { throw new Error(); }); @@ -220,7 +267,7 @@ describe('GCPFunction', () => { // integration is included in the defaults and the necessary data is stored in `sdkProcessingMetadata`. The // integration's tests cover testing that it uses that data correctly. test('wrapHttpFunction request data prereqs', async () => { - Sentry.GCPFunction.init({}); + init({}); const handler: HttpFunction = (_req, res) => { res.end(); @@ -229,13 +276,12 @@ describe('GCPFunction', () => { await handleHttp(wrappedHandler); - const initOptions = (SentryNode.init as unknown as jest.SpyInstance).mock.calls[0]; + const initOptions = (mockInit as unknown as jest.SpyInstance).mock.calls[0]; const defaultIntegrations = initOptions[0].defaultIntegrations.map((i: Integration) => i.name); expect(defaultIntegrations).toContain('RequestData'); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setSDKProcessingMetadata).toHaveBeenCalledWith({ + expect(mockScope.setSDKProcessingMetadata).toHaveBeenCalledWith({ request: { method: 'POST', url: '/path?q=query', @@ -263,10 +309,9 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); }); test('capture error', async () => { @@ -286,11 +331,10 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalled(); }); }); @@ -314,10 +358,9 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); }); test('capture error', async () => { @@ -341,11 +384,10 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalled(); }); }); @@ -366,10 +408,9 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); }); test('capture error', async () => { @@ -389,11 +430,10 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalled(); }); test('capture exception', async () => { @@ -413,8 +453,8 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); }); }); @@ -426,10 +466,9 @@ describe('GCPFunction', () => { const wrappedHandler = wrapEventFunction(handler); await expect(handleEvent(wrappedHandler)).rejects.toThrowError(error); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error just mocking around... - const scopeFunction = SentryNode.captureException.mock.calls[0][1]; + const scopeFunction = mockCaptureException.mock.calls[0][1]; const event: Event = { exception: { values: [{}] } }; let evtProcessor: ((e: Event) => Event) | undefined = undefined; scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) }); @@ -446,8 +485,7 @@ describe('GCPFunction', () => { const handler: EventFunction = (_data, _context) => 42; const wrappedHandler = wrapEventFunction(handler); await handleEvent(wrappedHandler); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setContext).toBeCalledWith('gcp.function.context', { + expect(mockScope.setContext).toBeCalledWith('gcp.function.context', { eventType: 'event.type', resource: 'some.resource', }); @@ -470,10 +508,9 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); }); test('capture error', async () => { @@ -493,11 +530,10 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalled(); }); }); @@ -518,10 +554,9 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalledWith(2000); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalledWith(2000); }); test('capture error', async () => { @@ -541,11 +576,10 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.end).toBeCalled(); - expect(SentryNode.flush).toBeCalled(); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); + expect(mockSpan.end).toBeCalled(); + expect(mockFlush).toBeCalled(); }); test('capture exception', async () => { @@ -565,8 +599,8 @@ describe('GCPFunction', () => { }, }; - expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); - expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); + expect(mockStartSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); + expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); }); }); @@ -574,15 +608,14 @@ describe('GCPFunction', () => { const handler: CloudEventFunction = _context => 42; const wrappedHandler = wrapCloudEventFunction(handler); await handleCloudEvent(wrappedHandler); - // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' }); + expect(mockScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' }); }); describe('init()', () => { test('calls Sentry.init with correct sdk info metadata', () => { - Sentry.GCPFunction.init({}); + init({}); - expect(Sentry.init).toBeCalledWith( + expect(mockInit).toBeCalledWith( expect.objectContaining({ _metadata: { sdk: { @@ -591,10 +624,10 @@ describe('GCPFunction', () => { packages: [ { name: 'npm:@sentry/serverless', - version: '6.6.6', + version: expect.any(String), }, ], - version: '6.6.6', + version: expect.any(String), }, }, }), diff --git a/packages/serverless/test/google-cloud-grpc.test.ts b/packages/serverless/test/google-cloud-grpc.test.ts index 8c0e0866bf0c..c2cd1b3167db 100644 --- a/packages/serverless/test/google-cloud-grpc.test.ts +++ b/packages/serverless/test/google-cloud-grpc.test.ts @@ -5,15 +5,28 @@ import { EventEmitter } from 'events'; import * as fs from 'fs'; import * as path from 'path'; import { PubSub } from '@google-cloud/pubsub'; -import * as SentryNode from '@sentry/node'; import * as http2 from 'http2'; import * as nock from 'nock'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { GoogleCloudGrpc } from '../src/google-cloud-grpc'; +import { NodeClient, createTransport, setCurrentClient } from '@sentry/node'; +import { googleCloudGrpcIntegration } from '../src/google-cloud-grpc'; const spyConnect = jest.spyOn(http2, 'connect'); +const mockSpanEnd = jest.fn(); +const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs })); + +jest.mock('@sentry/node', () => { + return { + ...jest.requireActual('@sentry/node'), + startInactiveSpan: (ctx: unknown) => { + mockStartInactiveSpan(ctx); + return { end: mockSpanEnd }; + }, + }; +}); + /** Fake HTTP2 stream */ class FakeStream extends EventEmitter { public rstCode: number = 0; @@ -71,18 +84,24 @@ function mockHttp2Session(): FakeSession { } describe('GoogleCloudGrpc tracing', () => { - beforeAll(() => { - new GoogleCloudGrpc().setupOnce(); + const mockClient = new NodeClient({ + tracesSampleRate: 1.0, + integrations: [], + dsn: 'https://withAWSServices@domain/123', + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], }); + const integration = googleCloudGrpcIntegration(); + mockClient.addIntegration(integration); + beforeEach(() => { nock('https://www.googleapis.com').post('/oauth2/v4/token').reply(200, []); + setCurrentClient(mockClient); + mockSpanEnd.mockClear(); + mockStartInactiveSpan.mockClear(); }); - afterEach(() => { - // @ts-expect-error see "Why @ts-expect-error" note - SentryNode.resetMocks(); - spyConnect.mockClear(); - }); + afterAll(() => { nock.restore(); spyConnect.mockRestore(); @@ -116,18 +135,21 @@ describe('GoogleCloudGrpc tracing', () => { resolveTxt.mockReset(); }); + afterAll(async () => { + await pubsub.close(); + }); + test('publish', async () => { mockHttp2Session().mockUnaryRequest(Buffer.from('00000000120a1031363337303834313536363233383630', 'hex')); const resp = await pubsub.topic('nicetopic').publish(Buffer.from('data')); expect(resp).toEqual('1637084156623860'); - expect(SentryNode.startInactiveSpan).toBeCalledWith({ + expect(mockStartInactiveSpan).toBeCalledWith({ op: 'grpc.pubsub', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', }, name: 'unary call publish', }); - await pubsub.close(); }); }); }); diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts index 748841e58579..6d64bd68624f 100644 --- a/packages/serverless/test/google-cloud-http.test.ts +++ b/packages/serverless/test/google-cloud-http.test.ts @@ -1,25 +1,46 @@ import * as fs from 'fs'; import * as path from 'path'; import { BigQuery } from '@google-cloud/bigquery'; -import * as SentryNode from '@sentry/node'; import * as nock from 'nock'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { GoogleCloudHttp } from '../src/google-cloud-http'; +import { NodeClient, createTransport, setCurrentClient } from '@sentry/node'; +import { googleCloudHttpIntegration } from '../src/google-cloud-http'; + +const mockSpanEnd = jest.fn(); +const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs })); + +jest.mock('@sentry/node', () => { + return { + ...jest.requireActual('@sentry/node'), + startInactiveSpan: (ctx: unknown) => { + mockStartInactiveSpan(ctx); + return { end: mockSpanEnd }; + }, + }; +}); describe('GoogleCloudHttp tracing', () => { - beforeAll(() => { - new GoogleCloudHttp().setupOnce(); + const mockClient = new NodeClient({ + tracesSampleRate: 1.0, + integrations: [], + dsn: 'https://withAWSServices@domain/123', + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], }); + + const integration = googleCloudHttpIntegration(); + mockClient.addIntegration(integration); + beforeEach(() => { nock('https://www.googleapis.com') .post('/oauth2/v4/token') .reply(200, '{"access_token":"a.b.c","expires_in":3599,"token_type":"Bearer"}'); + setCurrentClient(mockClient); + mockSpanEnd.mockClear(); + mockStartInactiveSpan.mockClear(); }); - afterEach(() => { - // @ts-expect-error see "Why @ts-expect-error" note - SentryNode.resetMocks(); - }); + afterAll(() => { nock.restore(); }); @@ -51,14 +72,14 @@ describe('GoogleCloudHttp tracing', () => { ); const resp = await bigquery.query('SELECT true AS foo'); expect(resp).toEqual([[{ foo: true }]]); - expect(SentryNode.startInactiveSpan).toBeCalledWith({ + expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', name: 'POST /jobs', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, }); - expect(SentryNode.startInactiveSpan).toBeCalledWith({ + expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', name: expect.stringMatching(/^GET \/queries\/.+/), attributes: { From 07a2264bba50d4916dc9e357b48665c0085c3ec9 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 30 Jan 2024 08:01:03 +0100 Subject: [PATCH 28/39] feat(core): Read propagation context off scopes in `startSpan` APIs (#10300) --- packages/core/src/tracing/trace.ts | 63 ++++++++++++++++---- packages/core/test/lib/tracing/trace.test.ts | 50 ++++++++++++++++ 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index ad1b95345ef9..bdeba55ed9e4 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -5,6 +5,7 @@ import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/ut import { DEBUG_BUILD } from '../debug-build'; import { getCurrentScope, withScope } from '../exports'; import type { Hub } from '../hub'; +import { getIsolationScope } from '../hub'; import { getCurrentHub } from '../hub'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; @@ -172,11 +173,32 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { ? // eslint-disable-next-line deprecation/deprecation context.scope.getSpan() : getActiveSpan(); - return parentSpan - ? // eslint-disable-next-line deprecation/deprecation - parentSpan.startChild(ctx) - : // eslint-disable-next-line deprecation/deprecation - hub.startTransaction(ctx); + + if (parentSpan) { + // eslint-disable-next-line deprecation/deprecation + return parentSpan.startChild(ctx); + } else { + const isolationScope = getIsolationScope(); + const scope = getCurrentScope(); + + const { traceId, dsc, parentSpanId, sampled } = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; + + // eslint-disable-next-line deprecation/deprecation + return hub.startTransaction({ + traceId, + parentSpanId, + parentSampled: sampled, + ...ctx, + metadata: { + dynamicSamplingContext: dsc, + // eslint-disable-next-line deprecation/deprecation + ...ctx.metadata, + }, + }); + } } /** @@ -256,11 +278,32 @@ function createChildSpanOrTransaction( if (!hasTracingEnabled()) { return undefined; } - return parentSpan - ? // eslint-disable-next-line deprecation/deprecation - parentSpan.startChild(ctx) - : // eslint-disable-next-line deprecation/deprecation - hub.startTransaction(ctx); + + if (parentSpan) { + // eslint-disable-next-line deprecation/deprecation + return parentSpan.startChild(ctx); + } else { + const isolationScope = getIsolationScope(); + const scope = getCurrentScope(); + + const { traceId, dsc, parentSpanId, sampled } = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; + + // eslint-disable-next-line deprecation/deprecation + return hub.startTransaction({ + traceId, + parentSpanId, + parentSampled: sampled, + ...ctx, + metadata: { + dynamicSamplingContext: dsc, + // eslint-disable-next-line deprecation/deprecation + ...ctx.metadata, + }, + }); + } } /** diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index f2103891b4f5..6bca44c6b088 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -5,6 +5,7 @@ import { getCurrentScope, makeMain, spanToJSON, + withScope, } from '../../../src'; import { Scope } from '../../../src/scope'; import { @@ -318,6 +319,22 @@ describe('startSpan', () => { expect(getCurrentScope()).toBe(initialScope); expect(getActiveSpan()).toBe(undefined); }); + + it("picks up the trace id off the parent scope's propagation context", () => { + expect.assertions(1); + withScope(scope => { + scope.setPropagationContext({ + traceId: '99999999999999999999999999999999', + spanId: '1212121212121212', + dsc: {}, + parentSpanId: '4242424242424242', + }); + + startSpan({ name: 'span' }, span => { + expect(span?.spanContext().traceId).toBe('99999999999999999999999999999999'); + }); + }); + }); }); describe('startSpanManual', () => { @@ -381,6 +398,23 @@ describe('startSpanManual', () => { expect(start).toEqual(1234); }); + + it("picks up the trace id off the parent scope's propagation context", () => { + expect.assertions(1); + withScope(scope => { + scope.setPropagationContext({ + traceId: '99999999999999999999999999999991', + spanId: '1212121212121212', + dsc: {}, + parentSpanId: '4242424242424242', + }); + + startSpanManual({ name: 'span' }, span => { + expect(span?.spanContext().traceId).toBe('99999999999999999999999999999991'); + span?.end(); + }); + }); + }); }); describe('startInactiveSpan', () => { @@ -429,6 +463,22 @@ describe('startInactiveSpan', () => { const span = startInactiveSpan({ name: 'outer', startTime: [1234, 0] }); expect(spanToJSON(span!).start_timestamp).toEqual(1234); }); + + it("picks up the trace id off the parent scope's propagation context", () => { + expect.assertions(1); + withScope(scope => { + scope.setPropagationContext({ + traceId: '99999999999999999999999999999991', + spanId: '1212121212121212', + dsc: {}, + parentSpanId: '4242424242424242', + }); + + const span = startInactiveSpan({ name: 'span' }); + expect(span?.spanContext().traceId).toBe('99999999999999999999999999999991'); + span?.end(); + }); + }); }); describe('continueTrace', () => { From 2267493882d7e05236b27e08a850e07a568071d5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 30 Jan 2024 09:37:32 +0100 Subject: [PATCH 29/39] feat(remix): Add more missing `@sentry/node` re-exports (#10391) Missed some exports in my manual pass in #10385. Test in #10389 discovered more missing exports which this PR adds or marks as unnecessary in the re-export test. --- .../scripts/consistentExports.ts | 7 +------ packages/remix/src/index.server.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index a488ef463412..69eee6df5571 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -22,8 +22,6 @@ const NODE_EXPORTS_IGNORE = [ 'DebugSession', 'AnrIntegrationOptions', 'LocalVariablesIntegrationOptions', - // deprecated - 'spanStatusfromHttpCode', ]; type Dependent = { @@ -43,13 +41,10 @@ const DEPENDENTS: Dependent[] = [ // Next.js doesn't require explicit exports, so we can just merge top level and `default` exports: // @ts-expect-error: `default` is not in the type definition but it's defined exports: Object.keys({ ...SentryNextJs, ...SentryNextJs.default }), - ignoreExports: ['withSentryConfig'], }, { package: '@sentry/remix', exports: Object.keys(SentryRemix), - // TODO: Fix exports in remix - skip: true, }, { package: '@sentry/serverless', @@ -58,9 +53,9 @@ const DEPENDENTS: Dependent[] = [ // Deprecated, no need to add this now to serverless 'extractTraceparentData', 'getModuleFromFilename', + 'enableAnrDetection', // TODO: Should these be exported from serverless? 'cron', - 'enableAnrDetection', 'runWithAsyncContext', 'hapiErrorPlugin', ], diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 457c0e3a523b..da1e794690de 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -39,6 +39,7 @@ export { // eslint-disable-next-line deprecation/deprecation makeMain, setCurrentClient, + NodeClient, Scope, // eslint-disable-next-line deprecation/deprecation startTransaction, @@ -83,6 +84,18 @@ export { isInitialized, cron, parameterize, + metrics, + // eslint-disable-next-line deprecation/deprecation + getModuleFromFilename, + createGetModuleFromFilename, + functionToStringIntegration, + hapiErrorPlugin, + inboundFiltersIntegration, + linkedErrorsIntegration, + requestDataIntegration, + runWithAsyncContext, + // eslint-disable-next-line deprecation/deprecation + enableAnrDetection, } from '@sentry/node'; // Keeping the `*` exports for backwards compatibility and types From 0605f177e622be98b0d273de905eb2099920ace1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 30 Jan 2024 09:38:10 +0100 Subject: [PATCH 30/39] feat(sveltekit): Add more missing `@sentry/node` re-exports (#10392) Adds missing re-exports from `@sentry/node` in the sveltekit server SDK and enables the re-export test for @sentry/sveltekit --- .../scripts/consistentExports.ts | 2 -- packages/sveltekit/src/server/index.ts | 13 +++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 69eee6df5571..b2dd286bc5af 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -65,8 +65,6 @@ const DEPENDENTS: Dependent[] = [ { package: '@sentry/sveltekit', exports: Object.keys(SentrySvelteKit), - // TODO: Fix exports in sveltekit - skip: true, }, ]; diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index eb021c6c76cd..32fcec426df4 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -30,6 +30,7 @@ export { getGlobalScope, getIsolationScope, Hub, + NodeClient, // eslint-disable-next-line deprecation/deprecation makeMain, setCurrentClient, @@ -78,6 +79,18 @@ export { continueTrace, cron, parameterize, + // eslint-disable-next-line deprecation/deprecation + getModuleFromFilename, + createGetModuleFromFilename, + functionToStringIntegration, + hapiErrorPlugin, + inboundFiltersIntegration, + linkedErrorsIntegration, + requestDataIntegration, + metrics, + runWithAsyncContext, + // eslint-disable-next-line deprecation/deprecation + enableAnrDetection, } from '@sentry/node'; // We can still leave this for the carrier init and type exports From 18c0a4f8e4212a635ba74d83c11056fb3a344ea5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 30 Jan 2024 09:38:56 +0100 Subject: [PATCH 31/39] feat(bun): Add missing `@sentry/node` re-exports (#10396) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing exports from the Node to the Bun SDK. Ignored a couple of exports that I believe aren't compatible with Bun or for which we have dedicated Bun replacements (e.g. client, transport). Node would throw ESM/CJS errors when importing from `@sentry/bun`. So let's just use Bun for this test 🙃 --- .github/workflows/build.yml | 3 +++ .../node-exports-test-app/package.json | 4 ++-- .../scripts/consistentExports.ts | 13 ++++++++++++- packages/bun/src/index.ts | 17 ++++++++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00459abbe701..62b713e73def 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -947,6 +947,9 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: 'dev-packages/e2e-tests/package.json' + - name: Set up Bun + if: matrix.test-application == 'node-exports-test-app' + uses: oven-sh/setup-bun@v1 - name: Restore caches uses: ./.github/actions/restore-cache env: diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json index 056dd6836e61..8965bb7de982 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "build": "tsc", - "start": "pnpm build && node dist/consistentExports.js", - "test": " node dist/consistentExports.js", + "start": "pnpm build && bun run ./dist/consistentExports.js", + "test": " bun run ./dist/consistentExports.js", "clean": "npx rimraf node_modules,pnpm-lock.yaml,dist", "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test" diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index b2dd286bc5af..f77b14445d57 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -1,5 +1,5 @@ import * as SentryAstro from '@sentry/astro'; -// import * as SentryBun from '@sentry/bun'; +import * as SentryBun from '@sentry/bun'; import * as SentryNextJs from '@sentry/nextjs'; import * as SentryNode from '@sentry/node'; import * as SentryRemix from '@sentry/remix'; @@ -36,6 +36,17 @@ const DEPENDENTS: Dependent[] = [ package: '@sentry/astro', exports: Object.keys(SentryAstro), }, + { + package: '@sentry/bun', + exports: Object.keys(SentryBun), + ignoreExports: [ + // not supported in bun: + 'Handlers', + 'NodeClient', + 'hapiErrorPlugin', + 'makeNodeTransport', + ], + }, { package: '@sentry/nextjs', // Next.js doesn't require explicit exports, so we can just merge top level and `default` exports: diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 9869a52caaa9..5742597485e0 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -89,7 +89,22 @@ export { parameterize, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; -export { autoDiscoverNodePerformanceMonitoringIntegrations, cron } from '@sentry/node'; +export { + // eslint-disable-next-line deprecation/deprecation + deepReadDirSync, + // eslint-disable-next-line deprecation/deprecation + enableAnrDetection, + // eslint-disable-next-line deprecation/deprecation + getModuleFromFilename, + DEFAULT_USER_INCLUDES, + autoDiscoverNodePerformanceMonitoringIntegrations, + cron, + createGetModuleFromFilename, + defaultStackParser, + extractRequestData, + getSentryRelease, + addRequestDataToEvent, +} from '@sentry/node'; export { BunClient } from './client'; export { From 1fea33968986592db89cd961b4b741c92df9f957 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 30 Jan 2024 11:11:56 +0100 Subject: [PATCH 32/39] feat(serverless): Add missing `@sentry/node` re-exports (#10390) Adds missing re-exports to the serverless package --- .../scripts/consistentExports.ts | 15 ++------------- packages/serverless/src/index.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index f77b14445d57..cf8233680c11 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -11,7 +11,7 @@ const NODE_EXPORTS_IGNORE = [ 'default', // Probably generated by transpilation, no need to require it '__esModule', - // this function was deprecates almost immediately after it was introduced + // this function was deprecated almost immediately after it was introduced // due to a name change (startSpan). No need to re-export it IMHO. 'startActiveSpan', // this was never meant for external use (and documented as such) @@ -60,18 +60,7 @@ const DEPENDENTS: Dependent[] = [ { package: '@sentry/serverless', exports: Object.keys(SentryServerless), - ignoreExports: [ - // Deprecated, no need to add this now to serverless - 'extractTraceparentData', - 'getModuleFromFilename', - 'enableAnrDetection', - // TODO: Should these be exported from serverless? - 'cron', - 'runWithAsyncContext', - 'hapiErrorPlugin', - ], - // TODO: Fix exports in serverless - skip: true, + ignoreExports: ['cron', 'hapiErrorPlugin', 'enableAnrDetection'], }, { package: '@sentry/sveltekit', diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 8db1e4ba5be0..abc135a6b750 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -39,6 +39,9 @@ export { getIsolationScope, getHubFromCarrier, // eslint-disable-next-line deprecation/deprecation + spanStatusfromHttpCode, + getSpanStatusFromHttpCode, + // eslint-disable-next-line deprecation/deprecation makeMain, setCurrentClient, setContext, @@ -79,4 +82,15 @@ export { startSpanManual, continueTrace, parameterize, + requestDataIntegration, + linkedErrorsIntegration, + inboundFiltersIntegration, + functionToStringIntegration, + // eslint-disable-next-line deprecation/deprecation + getModuleFromFilename, + createGetModuleFromFilename, + metrics, + // eslint-disable-next-line deprecation/deprecation + extractTraceparentData, + runWithAsyncContext, } from '@sentry/node'; From 1822af50a90f8b85553152ed539c988d92de6ed0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 30 Jan 2024 11:50:46 +0100 Subject: [PATCH 33/39] feat(core): Ensure `startSpan()` can handle spans that require parent (#10386) This introduces a new `onlyIfParent` option to the start-span APIs, which, if set, will ensure to not create a span if there is no parent. I've added this also for otel to demonstrate this works there as well. This PR also uses this option in a few places, esp. for http.client spans, to ensure we do not capture these if there is no ongoing transaction. This replaces https://github.com/getsentry/sentry-javascript/pull/10375/files with a more generic solution, and includes the test from https://github.com/getsentry/sentry-javascript/pull/10376 (which passes here as well). --- .../request/fetch-with-no-active-span/init.js | 10 +++ .../fetch-with-no-active-span/subject.js | 1 + .../request/fetch-with-no-active-span/test.ts | 35 +++++++++++ .../request/xhr-with-no-active-span/init.js | 10 +++ .../xhr-with-no-active-span/subject.js | 11 ++++ .../request/xhr-with-no-active-span/test.ts | 35 +++++++++++ .../utils/helpers.ts | 2 +- packages/core/src/tracing/trace.ts | 14 ++++- packages/core/test/lib/tracing/trace.test.ts | 62 ++++++++++++++++++ packages/opentelemetry/src/trace.ts | 20 +++++- packages/opentelemetry/src/types.ts | 1 + packages/opentelemetry/test/trace.test.ts | 63 +++++++++++++++++++ packages/react/src/profiler.tsx | 5 ++ packages/react/test/profiler.test.tsx | 6 ++ packages/serverless/src/awsservices.ts | 1 + packages/serverless/src/google-cloud-grpc.ts | 1 + packages/serverless/src/google-cloud-http.ts | 1 + packages/serverless/test/awsservices.test.ts | 3 + .../serverless/test/google-cloud-grpc.test.ts | 1 + .../serverless/test/google-cloud-http.test.ts | 2 + .../tracing-internal/src/browser/request.ts | 29 ++++----- packages/tracing-internal/src/common/fetch.ts | 29 ++++----- .../src/node/integrations/prisma.ts | 1 + .../test/integrations/node/prisma.test.ts | 1 + packages/types/src/startSpanOptions.ts | 3 + 25 files changed, 309 insertions(+), 38 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js new file mode 100644 index 000000000000..92152554ea57 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + // disable pageload transaction + integrations: [Sentry.BrowserTracing({ tracingOrigins: ['http://example.com'], startTransactionOnPageLoad: false })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js new file mode 100644 index 000000000000..f62499b1e9c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts new file mode 100644 index 000000000000..4dc5a0ac4e0a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeUrlRegex, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'there should be no span created for fetch requests with no active span', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + let requestCount = 0; + page.on('request', request => { + expect(envelopeUrlRegex.test(request.url())).toBe(false); + requestCount++; + }); + + await page.goto(url); + + // Here are the requests that should exist: + // 1. HTML page + // 2. Init JS bundle + // 3. Subject JS bundle + // 4 [OPTIONAl] CDN JS bundle + // and then 3 fetch requests + if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { + expect(requestCount).toBe(7); + } else { + expect(requestCount).toBe(6); + } + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js new file mode 100644 index 000000000000..92152554ea57 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + // disable pageload transaction + integrations: [Sentry.BrowserTracing({ tracingOrigins: ['http://example.com'], startTransactionOnPageLoad: false })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js new file mode 100644 index 000000000000..5790c230aa66 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js @@ -0,0 +1,11 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', 'http://example.com/0'); +xhr_1.send(); + +const xhr_2 = new XMLHttpRequest(); +xhr_2.open('GET', 'http://example.com/1'); +xhr_2.send(); + +const xhr_3 = new XMLHttpRequest(); +xhr_3.open('GET', 'http://example.com/2'); +xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts new file mode 100644 index 000000000000..19c1f5891a39 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeUrlRegex, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'there should be no span created for xhr requests with no active span', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + let requestCount = 0; + page.on('request', request => { + expect(envelopeUrlRegex.test(request.url())).toBe(false); + requestCount++; + }); + + await page.goto(url); + + // Here are the requests that should exist: + // 1. HTML page + // 2. Init JS bundle + // 3. Subject JS bundle + // 4 [OPTIONAl] CDN JS bundle + // and then 3 fetch requests + if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { + expect(requestCount).toBe(7); + } else { + expect(requestCount).toBe(6); + } + }, +); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index d00f125a90c1..6027695746e9 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -1,7 +1,7 @@ import type { Page, Request } from '@playwright/test'; import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/types'; -const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; +export const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; export const envelopeParser = (request: Request | null): unknown[] => { // https://develop.sentry.dev/sdk/envelopes/ diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index bdeba55ed9e4..308e68a9738c 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -80,7 +80,9 @@ export function startSpan(context: StartSpanOptions, callback: (span: Span | // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); - const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); @@ -128,7 +130,9 @@ export function startSpanManual( // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); - const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); @@ -174,6 +178,12 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { context.scope.getSpan() : getActiveSpan(); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + + if (shouldSkipSpan) { + return undefined; + } + if (parentSpan) { // eslint-disable-next-line deprecation/deprecation return parentSpan.startChild(ctx); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 6bca44c6b088..fca086f10c94 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -335,6 +335,28 @@ describe('startSpan', () => { }); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).toBeUndefined(); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeDefined(); + }); + }); }); describe('startSpanManual', () => { @@ -415,6 +437,28 @@ describe('startSpanManual', () => { }); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).toBeUndefined(); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeDefined(); + }); + }); }); describe('startInactiveSpan', () => { @@ -479,6 +523,24 @@ describe('startInactiveSpan', () => { span?.end(); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + expect(span).toBeUndefined(); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + return span; + }); + + expect(span).toBeDefined(); + }); + }); }); describe('continueTrace', () => { diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index e2f517900114..1047d77f39fd 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -1,5 +1,7 @@ import type { Span, Tracer } from '@opentelemetry/api'; +import { context } from '@opentelemetry/api'; import { SpanStatusCode, trace } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; import { SDK_VERSION, handleCallbackErrors } from '@sentry/core'; import type { Client } from '@sentry/types'; @@ -22,7 +24,11 @@ export function startSpan(spanContext: OpenTelemetrySpanContext, callback: (s const { name } = spanContext; - return tracer.startActiveSpan(name, spanContext, span => { + const activeCtx = context.active(); + const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx); + const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx; + + return tracer.startActiveSpan(name, spanContext, ctx, span => { _applySentryAttributesToSpan(span, spanContext); return handleCallbackErrors( @@ -49,7 +55,11 @@ export function startSpanManual(spanContext: OpenTelemetrySpanContext, callba const { name } = spanContext; - return tracer.startActiveSpan(name, spanContext, span => { + const activeCtx = context.active(); + const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx); + const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx; + + return tracer.startActiveSpan(name, spanContext, ctx, span => { _applySentryAttributesToSpan(span, spanContext); return handleCallbackErrors( @@ -81,7 +91,11 @@ export function startInactiveSpan(spanContext: OpenTelemetrySpanContext): Span { const { name } = spanContext; - const span = tracer.startSpan(name, spanContext); + const activeCtx = context.active(); + const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx); + const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx; + + const span = tracer.startSpan(name, spanContext, ctx); _applySentryAttributesToSpan(span, spanContext); diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts index 168a9f4893a6..d01d80b64e82 100644 --- a/packages/opentelemetry/src/types.ts +++ b/packages/opentelemetry/src/types.ts @@ -14,6 +14,7 @@ export interface OpenTelemetrySpanContext { origin?: SpanOrigin; source?: TransactionSource; scope?: Scope; + onlyIfParent?: boolean; // Base SpanOptions we support attributes?: Attributes; diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index af8625a6534e..34e27c3c62f3 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -2,6 +2,7 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { TraceFlags, context, trace } from '@opentelemetry/api'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { Span as SpanClass } from '@opentelemetry/sdk-trace-base'; import type { PropagationContext } from '@sentry/types'; import { getClient } from '../src/custom/hub'; @@ -260,6 +261,28 @@ describe('trace', () => { }, ); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).not.toBeInstanceOf(SpanClass); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeInstanceOf(SpanClass); + }); + }); }); describe('startInactiveSpan', () => { @@ -349,6 +372,24 @@ describe('trace', () => { }); expect(getSpanKind(span)).toEqual(SpanKind.CLIENT); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + expect(span).not.toBeInstanceOf(SpanClass); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + return span; + }); + + expect(span).toBeInstanceOf(SpanClass); + }); + }); }); describe('startSpanManual', () => { @@ -419,6 +460,28 @@ describe('trace', () => { ); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).not.toBeInstanceOf(SpanClass); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeInstanceOf(SpanClass); + }); + }); }); describe('trace (tracing disabled)', () => { diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index d4f942e33d64..54ed3fa541c8 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -57,6 +57,7 @@ class Profiler extends React.Component { this._mountSpan = startInactiveSpan({ name: `<${name}>`, + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': name }, @@ -83,6 +84,7 @@ class Profiler extends React.Component { this._updateSpan = withActiveSpan(this._mountSpan, () => { return startInactiveSpan({ name: `<${this.props.name}>`, + onlyIfParent: true, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: now, @@ -115,6 +117,7 @@ class Profiler extends React.Component { const startTimestamp = spanToJSON(this._mountSpan).timestamp; withActiveSpan(this._mountSpan, () => { const renderSpan = startInactiveSpan({ + onlyIfParent: true, name: `<${name}>`, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', @@ -187,6 +190,7 @@ function useProfiler( return startInactiveSpan({ name: `<${name}>`, + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': name }, @@ -205,6 +209,7 @@ function useProfiler( const renderSpan = startInactiveSpan({ name: `<${name}>`, + onlyIfParent: true, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp, diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 2cdbae0e9320..5d399f342535 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -74,6 +74,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': 'unknown' }, @@ -92,6 +93,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenCalledTimes(2); expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: undefined, @@ -125,6 +127,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' }, name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: expect.any(Number), @@ -136,6 +139,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' }, name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: expect.any(Number), @@ -175,6 +179,7 @@ describe('useProfiler()', () => { expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ name: '', + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': 'Example' }, @@ -199,6 +204,7 @@ describe('useProfiler()', () => { expect(mockStartInactiveSpan).toHaveBeenLastCalledWith( expect.objectContaining({ name: '', + onlyIfParent: true, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': 'Example' }, diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 84e6ec93ff25..084fdbf0238a 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -71,6 +71,7 @@ function wrapMakeRequest( req.on('afterBuild', () => { span = startInactiveSpan({ name: describe(this, operation, params), + onlyIfParent: true, op: 'http.client', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 1423e2b3ad5a..a32ac9dae9ac 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -122,6 +122,7 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str } const span = startInactiveSpan({ name: `${callType} ${methodName}`, + onlyIfParent: true, op: `grpc.${serviceIdentifier}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index ee300e78a010..5e7558f4299d 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -64,6 +64,7 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { const span = SETUP_CLIENTS.has(getClient() as Client) ? startInactiveSpan({ name: `${httpMethod} ${reqOpts.uri}`, + onlyIfParent: true, op: `http.client.${identifyService(this.apiEndpoint)}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts index bbfc3240c3ac..3170f9056ec0 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/serverless/test/awsservices.test.ts @@ -61,6 +61,7 @@ describe('awsServicesIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, name: 'aws.s3.getObject foo', + onlyIfParent: true, }); expect(mockSpanEnd).toHaveBeenCalledTimes(1); @@ -84,6 +85,7 @@ describe('awsServicesIntegration', () => { expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client', name: 'aws.s3.getObject foo', + onlyIfParent: true, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, @@ -114,6 +116,7 @@ describe('awsServicesIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, name: 'aws.lambda.invoke foo', + onlyIfParent: true, }); expect(mockSpanEnd).toHaveBeenCalledTimes(1); }); diff --git a/packages/serverless/test/google-cloud-grpc.test.ts b/packages/serverless/test/google-cloud-grpc.test.ts index c2cd1b3167db..0ce19ee7db8b 100644 --- a/packages/serverless/test/google-cloud-grpc.test.ts +++ b/packages/serverless/test/google-cloud-grpc.test.ts @@ -149,6 +149,7 @@ describe('GoogleCloudGrpc tracing', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', }, name: 'unary call publish', + onlyIfParent: true, }); }); }); diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts index 6d64bd68624f..3389130565f9 100644 --- a/packages/serverless/test/google-cloud-http.test.ts +++ b/packages/serverless/test/google-cloud-http.test.ts @@ -75,6 +75,7 @@ describe('GoogleCloudHttp tracing', () => { expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', name: 'POST /jobs', + onlyIfParent: true, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, @@ -82,6 +83,7 @@ describe('GoogleCloudHttp tracing', () => { expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', name: expect.stringMatching(/^GET \/queries\/.+/), + onlyIfParent: true, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 89d5034a5425..d072188bb2af 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,7 +1,6 @@ /* eslint-disable max-lines */ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -280,21 +279,19 @@ export function xhrCallback( const scope = getCurrentScope(); const isolationScope = getIsolationScope(); - // only create a child span if there is an active span. This is because - // `startInactiveSpan` can still create a transaction under the hood - const span = - shouldCreateSpanResult && getActiveSpan() - ? startInactiveSpan({ - attributes: { - type: 'xhr', - 'http.method': sentryXhrData.method, - url: sentryXhrData.url, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', - }, - name: `${sentryXhrData.method} ${sentryXhrData.url}`, - op: 'http.client', - }) - : undefined; + const span = shouldCreateSpanResult + ? startInactiveSpan({ + name: `${sentryXhrData.method} ${sentryXhrData.url}`, + onlyIfParent: true, + attributes: { + type: 'xhr', + 'http.method': sentryXhrData.method, + url: sentryXhrData.url, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', + }, + op: 'http.client', + }) + : undefined; if (span) { xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 2177d1939987..e12ca3cf1b97 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,6 +1,5 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -82,21 +81,19 @@ export function instrumentFetchRequest( const { method, url } = handlerData.fetchData; - // only create a child span if there is an active span. This is because - // `startInactiveSpan` can still create a transaction under the hood - const span = - shouldCreateSpanResult && getActiveSpan() - ? startInactiveSpan({ - attributes: { - url, - type: 'fetch', - 'http.method': method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, - }, - name: `${method} ${url}`, - op: 'http.client', - }) - : undefined; + const span = shouldCreateSpanResult + ? startInactiveSpan({ + name: `${method} ${url}`, + onlyIfParent: true, + attributes: { + url, + type: 'fetch', + 'http.method': method, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + }, + op: 'http.client', + }) + : undefined; if (span) { handlerData.fetchData.__span = span.spanContext().spanId; diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index 4399778f1e80..2e2aa7dab17a 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -103,6 +103,7 @@ export class Prisma implements Integration { return startSpan( { name: model ? `${model} ${action}` : action, + onlyIfParent: true, op: 'db.prisma', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.prisma', diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts index c86f8aecabe2..2541eebf8f91 100644 --- a/packages/tracing/test/integrations/node/prisma.test.ts +++ b/packages/tracing/test/integrations/node/prisma.test.ts @@ -58,6 +58,7 @@ describe('setupOnce', function () { 'sentry.origin': 'auto.db.prisma', }, name: 'user create', + onlyIfParent: true, op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.prisma.version': '3.1.2', 'db.operation': 'create' }, }, diff --git a/packages/types/src/startSpanOptions.ts b/packages/types/src/startSpanOptions.ts index bde20c2c87bf..57ff96b3169f 100644 --- a/packages/types/src/startSpanOptions.ts +++ b/packages/types/src/startSpanOptions.ts @@ -14,6 +14,9 @@ export interface StartSpanOptions extends TransactionContext { /** The name of the span. */ name: string; + /** If set to true, only start a span if a parent span exists. */ + onlyIfParent?: boolean; + /** An op for the span. This is a categorization for spans. */ op?: string; From 71b8ac1ee222bdaa67d5a1c0016ae13154d1c6fa Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 30 Jan 2024 12:02:49 +0100 Subject: [PATCH 34/39] feat(tracing): Export proper type for browser tracing (#10411) To make it easier to extend this. --- .../src/browser/browserTracingIntegration.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 87169c55c8b5..e0ed5b790b63 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines, complexity */ import type { IdleTransaction } from '@sentry/core'; import { getClient } from '@sentry/core'; -import { defineIntegration, getCurrentHub } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, TRACING_DEFAULTS, @@ -151,8 +151,10 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { * * The integration can be configured with a variety of options, and can be extended to use * any routing library. This integration uses {@see IdleTransaction} to create transactions. + * + * We explicitly export the proper type here, as this has to be extended in some cases. */ -export const _browserTracingIntegration = ((_options: Partial = {}) => { +export const browserTracingIntegration = ((_options: Partial = {}) => { const _hasSetTracePropagationTargets = DEBUG_BUILD ? !!( // eslint-disable-next-line deprecation/deprecation @@ -389,8 +391,6 @@ export const _browserTracingIntegration = ((_options: Partial Date: Tue, 30 Jan 2024 13:00:09 +0100 Subject: [PATCH 35/39] Update README.md Mention bug bounty program in our README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index e1f87e04a07a..143da0220ff8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ convenient interface and improved consistency between various JavaScript environ - [Supported Platforms](#supported-platforms) - [Installation and Usage](#installation-and-usage) - [Other Packages](#other-packages) +- [Bug Bounty Program](#bug-bounty-program) ## Supported Platforms @@ -104,3 +105,12 @@ below: utility functions useful for various SDKs. - [`@sentry/types`](https://github.com/getsentry/sentry-javascript/tree/master/packages/types): Types used in all packages. + +## Bug Bounty Program + +We're excited to announce the expansion of our bug bounty program to include our SDK repositories. This program aims to improve the security of our open source projects by encouraging the community to identify and report potential security vulnerabilities. Your reward will depend on the severity of the identified vulnerability. + +Our program is currently running on an invitation basis. If you're interested in participating, please send us an email to security@sentry.io and tell us, that you are interested in auditing this repository. + +For more details, please have a look at https://sentry.io/security/#vulnerability-disclosure. + From 9fe67aaed54bd6989b8856f9832d1affe369bbdc Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 30 Jan 2024 13:23:26 +0100 Subject: [PATCH 36/39] fix: Fork scope and keep async context within `startSpan` and `startSpanManual` (#10413) --- packages/core/src/tracing/trace.ts | 99 ++++++++-------- packages/node/test/performance.test.ts | 150 +++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 47 deletions(-) create mode 100644 packages/node/test/performance.test.ts diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 308e68a9738c..dc822a2bab7d 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -5,6 +5,7 @@ import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/ut import { DEBUG_BUILD } from '../debug-build'; import { getCurrentScope, withScope } from '../exports'; import type { Hub } from '../hub'; +import { runWithAsyncContext } from '../hub'; import { getIsolationScope } from '../hub'; import { getCurrentHub } from '../hub'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; @@ -74,31 +75,33 @@ export function trace( export function startSpan(context: StartSpanOptions, callback: (span: Span | undefined) => T): T { const ctx = normalizeContext(context); - return withScope(context.scope, scope => { - // eslint-disable-next-line deprecation/deprecation - const hub = getCurrentHub(); - // eslint-disable-next-line deprecation/deprecation - const parentSpan = scope.getSpan(); + return runWithAsyncContext(() => { + return withScope(context.scope, scope => { + // eslint-disable-next-line deprecation/deprecation + const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + const parentSpan = scope.getSpan(); - const shouldSkipSpan = context.onlyIfParent && !parentSpan; - const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); - // eslint-disable-next-line deprecation/deprecation - scope.setSpan(activeSpan); - - return handleCallbackErrors( - () => callback(activeSpan), - () => { - // Only update the span status if it hasn't been changed yet - if (activeSpan) { - const { status } = spanToJSON(activeSpan); - if (!status || status === 'ok') { - activeSpan.setStatus('internal_error'); + // eslint-disable-next-line deprecation/deprecation + scope.setSpan(activeSpan); + + return handleCallbackErrors( + () => callback(activeSpan), + () => { + // Only update the span status if it hasn't been changed yet + if (activeSpan) { + const { status } = spanToJSON(activeSpan); + if (!status || status === 'ok') { + activeSpan.setStatus('internal_error'); + } } - } - }, - () => activeSpan && activeSpan.end(), - ); + }, + () => activeSpan && activeSpan.end(), + ); + }); }); } @@ -124,34 +127,36 @@ export function startSpanManual( ): T { const ctx = normalizeContext(context); - return withScope(context.scope, scope => { - // eslint-disable-next-line deprecation/deprecation - const hub = getCurrentHub(); - // eslint-disable-next-line deprecation/deprecation - const parentSpan = scope.getSpan(); - - const shouldSkipSpan = context.onlyIfParent && !parentSpan; - const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); + return runWithAsyncContext(() => { + return withScope(context.scope, scope => { + // eslint-disable-next-line deprecation/deprecation + const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation + const parentSpan = scope.getSpan(); - // eslint-disable-next-line deprecation/deprecation - scope.setSpan(activeSpan); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); - function finishAndSetSpan(): void { - activeSpan && activeSpan.end(); - } - - return handleCallbackErrors( - () => callback(activeSpan, finishAndSetSpan), - () => { - // Only update the span status if it hasn't been changed yet, and the span is not yet finished - if (activeSpan && activeSpan.isRecording()) { - const { status } = spanToJSON(activeSpan); - if (!status || status === 'ok') { - activeSpan.setStatus('internal_error'); + // eslint-disable-next-line deprecation/deprecation + scope.setSpan(activeSpan); + + function finishAndSetSpan(): void { + activeSpan && activeSpan.end(); + } + + return handleCallbackErrors( + () => callback(activeSpan, finishAndSetSpan), + () => { + // Only update the span status if it hasn't been changed yet, and the span is not yet finished + if (activeSpan && activeSpan.isRecording()) { + const { status } = spanToJSON(activeSpan); + if (!status || status === 'ok') { + activeSpan.setStatus('internal_error'); + } } - } - }, - ); + }, + ); + }); }); } diff --git a/packages/node/test/performance.test.ts b/packages/node/test/performance.test.ts new file mode 100644 index 000000000000..0f57dd4166e6 --- /dev/null +++ b/packages/node/test/performance.test.ts @@ -0,0 +1,150 @@ +import { setAsyncContextStrategy, setCurrentClient, startSpan, startSpanManual } from '@sentry/core'; +import type { TransactionEvent } from '@sentry/types'; +import { NodeClient, defaultStackParser } from '../src'; +import { setNodeAsyncContextStrategy } from '../src/async'; +import { getDefaultNodeClientOptions } from './helper/node-client-options'; + +const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; + +beforeAll(() => { + setNodeAsyncContextStrategy(); +}); + +afterAll(() => { + setAsyncContextStrategy(undefined); +}); + +describe('startSpan()', () => { + it('should correctly separate spans when called after one another with interwoven timings', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new NodeClient( + getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + tracesSampleRate: 1, + beforeSendTransaction: event => { + resolve(event); + return null; + }, + dsn, + }), + ), + ); + }); + + startSpan({ name: 'first' }, () => { + return new Promise(resolve => { + setTimeout(resolve, 500); + }); + }); + + startSpan({ name: 'second' }, () => { + return new Promise(resolve => { + setTimeout(resolve, 250); + }); + }); + + const transactionEvent = await transactionEventPromise; + + // Any transaction events happening shouldn't have any child spans + expect(transactionEvent.spans).toStrictEqual([]); + }); + + it('should correctly nest spans when called within one another', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new NodeClient( + getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + tracesSampleRate: 1, + beforeSendTransaction: event => { + resolve(event); + return null; + }, + dsn, + }), + ), + ); + }); + + startSpan({ name: 'first' }, () => { + startSpan({ name: 'second' }, () => undefined); + }); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toContainEqual(expect.objectContaining({ description: 'second' })); + }); +}); + +describe('startSpanManual()', () => { + it('should correctly separate spans when called after one another with interwoven timings', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new NodeClient( + getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + tracesSampleRate: 1, + beforeSendTransaction: event => { + resolve(event); + return null; + }, + dsn, + }), + ), + ); + }); + + startSpanManual({ name: 'first' }, span => { + return new Promise(resolve => { + setTimeout(() => { + span?.end(); + resolve(); + }, 500); + }); + }); + + startSpanManual({ name: 'second' }, span => { + return new Promise(resolve => { + setTimeout(() => { + span?.end(); + resolve(); + }, 500); + }); + }); + + const transactionEvent = await transactionEventPromise; + + // Any transaction events happening shouldn't have any child spans + expect(transactionEvent.spans).toStrictEqual([]); + }); + + it('should correctly nest spans when called within one another', async () => { + const transactionEventPromise = new Promise(resolve => { + setCurrentClient( + new NodeClient( + getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + tracesSampleRate: 1, + beforeSendTransaction: event => { + resolve(event); + return null; + }, + dsn, + }), + ), + ); + }); + + startSpanManual({ name: 'first' }, span1 => { + startSpanManual({ name: 'second' }, span2 => { + span2?.end(); + }); + span1?.end(); + }); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toContainEqual(expect.objectContaining({ description: 'second' })); + }); +}); From 689646c3d45f10c27e5c4cf2f316496c54c88d9e Mon Sep 17 00:00:00 2001 From: Stephanie Anderson Date: Tue, 30 Jan 2024 14:44:40 +0100 Subject: [PATCH 37/39] Update README.md Co-authored-by: Luca Forstner --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 143da0220ff8..b167b682655c 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ below: ## Bug Bounty Program -We're excited to announce the expansion of our bug bounty program to include our SDK repositories. This program aims to improve the security of our open source projects by encouraging the community to identify and report potential security vulnerabilities. Your reward will depend on the severity of the identified vulnerability. +Our bug bounty program aims to improve the security of our open source projects by encouraging the community to identify and report potential security vulnerabilities. Your reward will depend on the severity of the identified vulnerability. Our program is currently running on an invitation basis. If you're interested in participating, please send us an email to security@sentry.io and tell us, that you are interested in auditing this repository. From 2195ce6035a89968ba62cea4109df38e96335a0a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 30 Jan 2024 14:59:09 +0100 Subject: [PATCH 38/39] ref(tracing): Require to pass client to `startBrowserTracing*Span` utils (#10410) So we can be client-safe when extending the integration. --- .../src/browser/browserTracingIntegration.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index e0ed5b790b63..34fe4a7b13d2 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines, complexity */ import type { IdleTransaction } from '@sentry/core'; -import { getClient } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -11,6 +10,7 @@ import { startIdleTransaction, } from '@sentry/core'; import type { + Client, IntegrationFn, StartSpanOptions, Transaction, @@ -336,7 +336,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Mon, 29 Jan 2024 15:43:46 -0500 Subject: [PATCH 39/39] meta(changelog): Update changelog for 7.99.0 Apply suggestions from code review --- CHANGELOG.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23b13973847a..f7548880d5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.99.0 + +### Important Changes + +#### Deprecations + +This release includes some deprecations for span related methods and integrations in our Deno SDK, `@sentry/deno`. For +more details please look at our +[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md). + +- feat(core): Deprecate `Span.setHttpStatus` in favor of `setHttpStatus` (#10268) +- feat(core): Deprecate `spanStatusfromHttpCode` in favour of `getSpanStatusFromHttpCode` (#10361) +- feat(core): Deprecate `StartSpanOptions.origin` in favour of passing attribute (#10274) +- feat(deno): Expose functional integrations to replace classes (#10355) + +### Other Changes + +- feat(bun): Add missing `@sentry/node` re-exports (#10396) +- feat(core): Add `afterAllSetup` hook for integrations (#10345) +- feat(core): Ensure `startSpan()` can handle spans that require parent (#10386) +- feat(core): Read propagation context off scopes in `startSpan` APIs (#10300) +- feat(remix): Export missing `@sentry/node` functions (#10385, #10391) +- feat(serverless): Add missing `@sentry/node` re-exports (#10390) +- feat(sveltekit): Add more missing `@sentry/node` re-exports (#10392) +- feat(tracing): Export proper type for browser tracing (#10411) +- feat(tracing): Expose new `browserTracingIntegration` (#10351) +- fix: Ensure `afterAllSetup` is called when using `addIntegration()` (#10372) +- fix(core): Export `spanToTraceContext` function from span utils (#10364) +- fix(core): Make `FunctionToString` integration use SETUP_CLIENTS weakmap (#10358) +- fix(deno): Call function if client is not setup (#10354) +- fix(react): Fix attachReduxState option (#10381) +- fix(spotlight): Use unpatched http.request (#10369) +- fix(tracing): Only create request span if there is active span (#10375) +- ref: Read propagation context off of scope and isolation scope when propagating and applying trace context (#10297) + +Work in this release contributed by @AleshaOleg. Thank you for your contribution! + ## 7.98.0 This release primarily fixes some type declaration errors: @@ -20,7 +57,7 @@ Note: The 7.96.0 release was incomplete. This release is partially encompassing ## 7.96.0 -Note: This release was incomplete. Not all Sentry SDK packages were released for this version. Please upgrade to 7.97.0 +Note: This release was incomplete. Not all Sentry SDK packages were released for this version. Please upgrade to 7.98.0 directly. ### Important Changes