From facaae451e1b83961353ce7a586523b067402c08 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Sep 2024 10:28:37 +0200 Subject: [PATCH 01/22] feat(node): Use `@opentelemetry/instrumentation-undici` for fetch tracing (#13485) This PR migrates the `nativeNodeFetchIntegration` to use `@opentelemetry/instrumentation-undici` instead of `opentelemetry-instrumentation-fetch-node`. The instrumentation is still exported as `nativeNodeFetchIntegration` and is named `NodeFetch` to ensure backwards compatibility and the tests pass ~~without changes~~. Note: One `nextjs-14` e2e test did need a change due to the new/differing attribute names. It's worth noting that `@opentelemetry/instrumentation-undici` [uses different attributes](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2417#issuecomment-2329362506) from the latest semantic convention version vs what we are using and what's used by `opentelemetry-instrumentation-fetch-node`. It looks like the [http instrumentation is migrating to these too](https://github.com/open-telemetry/opentelemetry-js/pull/4940) so some of the changes in this PR will ensure that the http instrumentation continues to work after these updates. --- .github/dependabot.yml | 1 - .../tests/request-instrumentation.test.ts | 2 +- .../fetch-sampled-no-active-span/scenario.ts | 2 - .../requests/fetch-unsampled/scenario.ts | 2 - packages/bun/src/integrations/bunserver.ts | 3 +- packages/cloudflare/src/request.ts | 6 +- packages/core/src/semanticAttributes.ts | 4 + packages/node/package.json | 4 +- packages/node/src/integrations/node-fetch.ts | 141 ++++-------------- packages/opentelemetry/src/propagator.ts | 4 +- packages/opentelemetry/src/sampler.ts | 6 +- .../src/utils/isSentryRequest.ts | 4 +- .../src/utils/parseSpanDescription.ts | 14 +- yarn.lock | 39 ++--- 14 files changed, 63 insertions(+), 169 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 652d9daa2e39..d332007ee284 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,7 +20,6 @@ updates: - dependency-name: "@sentry/esbuild-plugin" - dependency-name: "@opentelemetry/*" - dependency-name: "@prisma/instrumentation" - - dependency-name: "opentelemetry-instrumentation-fetch-node" versioning-strategy: increase commit-message: prefix: feat 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 index 061c9d3cc5d6..65b9c4312d91 100644 --- 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 @@ -15,7 +15,7 @@ test('Should send a transaction with a fetch span', async ({ page }) => { expect(transactionEvent.spans).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - 'http.method': 'GET', + 'http.request.method': 'GET', 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.otel.node_fetch', }), diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.ts index 191797a10c15..9bbd9c9c1aeb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.ts @@ -11,8 +11,6 @@ Sentry.init({ }); async function run(): Promise { - // Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented - await new Promise(resolve => setTimeout(resolve, 100)); await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.ts index 91c38bf2b23a..9ab4c58967f2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.ts @@ -13,8 +13,6 @@ Sentry.init({ async function run(): Promise { // Wrap in span that is not sampled await Sentry.startSpan({ name: 'outer' }, async () => { - // Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented - await new Promise(resolve => setTimeout(resolve, 100)); await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 93a3f94dd4a0..193ae6f286ca 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, @@ -65,7 +66,7 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] const parsedUrl = parseUrl(request.url); const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve', - 'http.request.method': request.method || 'GET', + [SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: request.method || 'GET', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }; if (parsedUrl.search) { diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 7a474c3b27cb..1c51a08c194c 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -1,9 +1,11 @@ import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; import { + SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_URL_FULL, captureException, continueTrace, flush, @@ -45,8 +47,8 @@ export function wrapRequestHandler( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.cloudflare', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - ['http.request.method']: request.method, - ['url.full']: request.url, + [SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: request.method, + [SEMANTIC_ATTRIBUTE_URL_FULL]: request.url, }; const contentLength = request.headers.get('content-length'); diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 2c268110854c..2d24c52a15ea 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -41,3 +41,7 @@ export const SEMANTIC_ATTRIBUTE_CACHE_HIT = 'cache.hit'; export const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; export const SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE = 'cache.item_size'; + +/** TODO: Remove these once we update to latest semantic conventions */ +export const SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD = 'http.request.method'; +export const SEMANTIC_ATTRIBUTE_URL_FULL = 'url.full'; diff --git a/packages/node/package.json b/packages/node/package.json index 9a7091ef57fc..56ab710a1586 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -86,6 +86,7 @@ "@opentelemetry/instrumentation-nestjs-core": "0.40.0", "@opentelemetry/instrumentation-pg": "0.44.0", "@opentelemetry/instrumentation-redis-4": "0.42.0", + "@opentelemetry/instrumentation-undici": "0.5.0", "@opentelemetry/resources": "^1.25.1", "@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.25.1", @@ -99,9 +100,6 @@ "devDependencies": { "@types/node": "^14.18.0" }, - "optionalDependencies": { - "opentelemetry-instrumentation-fetch-node": "1.2.3" - }, "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", diff --git a/packages/node/src/integrations/node-fetch.ts b/packages/node/src/integrations/node-fetch.ts index fa7a9974135a..0726c2c63f9b 100644 --- a/packages/node/src/integrations/node-fetch.ts +++ b/packages/node/src/integrations/node-fetch.ts @@ -1,32 +1,9 @@ -import type { Span } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; -import { context, propagation } from '@opentelemetry/api'; -import { addBreadcrumb, defineIntegration, getCurrentScope, hasTracingEnabled } from '@sentry/core'; -import { - addOpenTelemetryInstrumentation, - generateSpanContextForPropagationContext, - getPropagationContextFromSpan, -} from '@sentry/opentelemetry'; +import type { UndiciRequest, UndiciResponse } from '@opentelemetry/instrumentation-undici'; +import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, addBreadcrumb, defineIntegration } from '@sentry/core'; +import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry'; import type { IntegrationFn, SanitizedRequestData } from '@sentry/types'; -import { getSanitizedUrlString, logger, parseUrl } from '@sentry/utils'; -import { DEBUG_BUILD } from '../debug-build'; -import { NODE_MAJOR } from '../nodeVersion'; - -import type { FetchInstrumentation } from 'opentelemetry-instrumentation-fetch-node'; - -import { addOriginToSpan } from '../utils/addOriginToSpan'; - -interface FetchRequest { - method: string; - origin: string; - path: string; - headers: string | string[]; -} - -interface FetchResponse { - headers: Buffer[]; - statusCode: number; -} +import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; interface NodeFetchOptions { /** @@ -46,106 +23,38 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => { const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; - async function getInstrumentation(): Promise { - // Only add NodeFetch if Node >= 18, as previous versions do not support it - if (NODE_MAJOR < 18) { - DEBUG_BUILD && logger.log('NodeFetch is not supported on Node < 18, skipping instrumentation...'); - return; - } - - try { - const pkg = await import('opentelemetry-instrumentation-fetch-node'); - const { FetchInstrumentation } = pkg; - - class SentryNodeFetchInstrumentation extends FetchInstrumentation { - // We extend this method so we have access to request _and_ response for the breadcrumb - public onHeaders({ request, response }: { request: FetchRequest; response: FetchResponse }): void { - if (_breadcrumbs) { - _addRequestBreadcrumb(request, response); - } - - return super.onHeaders({ request, response }); - } - } - - return new SentryNodeFetchInstrumentation({ - ignoreRequestHook: (request: FetchRequest) => { + return { + name: 'NodeFetch', + setupOnce() { + const instrumentation = new UndiciInstrumentation({ + requireParentforSpans: false, + ignoreRequestHook: request => { const url = getAbsoluteUrl(request.origin, request.path); - const tracingDisabled = !hasTracingEnabled(); const shouldIgnore = _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url); - if (shouldIgnore) { - return true; - } - - // If tracing is disabled, we still want to propagate traces - // So we do that manually here, matching what the instrumentation does otherwise - if (tracingDisabled) { - const ctx = context.active(); - const addedHeaders: Record = {}; - - // We generate a virtual span context from the active one, - // Where we attach the URL to the trace state, so the propagator can pick it up - const activeSpan = trace.getSpan(ctx); - const propagationContext = activeSpan - ? getPropagationContextFromSpan(activeSpan) - : getCurrentScope().getPropagationContext(); - - const spanContext = generateSpanContextForPropagationContext(propagationContext); - // We know that in practice we'll _always_ haven a traceState here - spanContext.traceState = spanContext.traceState?.set('sentry.url', url); - const ctxWithUrlTraceState = trace.setSpanContext(ctx, spanContext); - - propagation.inject(ctxWithUrlTraceState, addedHeaders); - - const requestHeaders = request.headers; - if (Array.isArray(requestHeaders)) { - Object.entries(addedHeaders).forEach(headers => requestHeaders.push(...headers)); - } else { - request.headers += Object.entries(addedHeaders) - .map(([k, v]) => `${k}: ${v}\r\n`) - .join(''); - } - - // Prevent starting a span for this request - return true; - } - - return false; + return !!shouldIgnore; }, - onRequest: ({ span }: { span: Span }) => { - _updateSpan(span); + startSpanHook: () => { + return { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.node_fetch', + }; + }, + responseHook: (_, { request, response }) => { + if (_breadcrumbs) { + addRequestBreadcrumb(request, response); + } }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - } catch (error) { - // Could not load instrumentation - DEBUG_BUILD && logger.log('Error while loading NodeFetch instrumentation: \n', error); - } - } - - return { - name: 'NodeFetch', - setupOnce() { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - getInstrumentation().then(instrumentation => { - if (instrumentation) { - addOpenTelemetryInstrumentation(instrumentation); - } }); + + addOpenTelemetryInstrumentation(instrumentation); }, }; }) satisfies IntegrationFn; export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchIntegration); -/** Update the span with data we need. */ -function _updateSpan(span: Span): void { - addOriginToSpan(span, 'auto.http.otel.node_fetch'); -} - /** Add a breadcrumb for outgoing requests. */ -function _addRequestBreadcrumb(request: FetchRequest, response: FetchResponse): void { +function addRequestBreadcrumb(request: UndiciRequest, response: UndiciResponse): void { const data = getBreadcrumbData(request); addBreadcrumb( @@ -165,7 +74,7 @@ function _addRequestBreadcrumb(request: FetchRequest, response: FetchResponse): ); } -function getBreadcrumbData(request: FetchRequest): Partial { +function getBreadcrumbData(request: UndiciRequest): Partial { try { const url = new URL(request.path, request.origin); const parsedUrl = parseUrl(url.toString()); diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 40b8a8139b0d..4ed5a15532d2 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -5,6 +5,7 @@ import { propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; import { SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; import type { continueTrace } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_URL_FULL } from '@sentry/core'; import { hasTracingEnabled } from '@sentry/core'; import { getRootSpan } from '@sentry/core'; import { spanToJSON } from '@sentry/core'; @@ -292,7 +293,8 @@ function getExistingBaggage(carrier: unknown): string | undefined { * 2. Else, if the active span has no URL attribute (e.g. it is unsampled), we check a special trace state (which we set in our sampler). */ function getCurrentURL(span: Span): string | undefined { - const urlAttribute = spanToJSON(span).data?.[SEMATTRS_HTTP_URL]; + const spanData = spanToJSON(span).data; + const urlAttribute = spanData?.[SEMATTRS_HTTP_URL] || spanData?.[SEMANTIC_ATTRIBUTE_URL_FULL]; if (urlAttribute) { return urlAttribute; } diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 446c325f3ac7..03d47989ae1d 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -5,8 +5,10 @@ import { TraceState } from '@opentelemetry/core'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { + SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_URL_FULL, hasTracingEnabled, sampleSpan, } from '@sentry/core'; @@ -54,7 +56,7 @@ export class SentrySampler implements Sampler { // but we want to leave downstream sampling decisions up to the server if ( spanKind === SpanKind.CLIENT && - spanAttributes[SEMATTRS_HTTP_METHOD] && + (spanAttributes[SEMATTRS_HTTP_METHOD] || spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]) && (!parentSpan || parentContext?.isRemote) ) { return wrapSamplingDecision({ decision: undefined, context, spanAttributes }); @@ -196,7 +198,7 @@ function getBaseTraceState(context: Context, spanAttributes: SpanAttributes): Tr let traceState = parentContext?.traceState || new TraceState(); // We always keep the URL on the trace state, so we can access it in the propagator - const url = spanAttributes[SEMATTRS_HTTP_URL]; + const url = spanAttributes[SEMATTRS_HTTP_URL] || spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL]; if (url && typeof url === 'string') { traceState = traceState.set(SENTRY_TRACE_STATE_URL, url); } diff --git a/packages/opentelemetry/src/utils/isSentryRequest.ts b/packages/opentelemetry/src/utils/isSentryRequest.ts index 3381b7833cea..c8b11c9e680c 100644 --- a/packages/opentelemetry/src/utils/isSentryRequest.ts +++ b/packages/opentelemetry/src/utils/isSentryRequest.ts @@ -1,5 +1,5 @@ import { SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; -import { getClient, isSentryRequestUrl } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_URL_FULL, getClient, isSentryRequestUrl } from '@sentry/core'; import type { AbstractSpan } from '../types'; import { spanHasAttributes } from './spanTypes'; @@ -16,7 +16,7 @@ export function isSentryRequestSpan(span: AbstractSpan): boolean { const { attributes } = span; - const httpUrl = attributes[SEMATTRS_HTTP_URL]; + const httpUrl = attributes[SEMATTRS_HTTP_URL] || attributes[SEMANTIC_ATTRIBUTE_URL_FULL]; if (!httpUrl) { return false; diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts index b600b81f8aec..07354313f331 100644 --- a/packages/opentelemetry/src/utils/parseSpanDescription.ts +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -14,7 +14,12 @@ import { import type { SpanAttributes, TransactionSource } from '@sentry/types'; import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_URL_FULL, +} from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '../semanticAttributes'; import type { AbstractSpan } from '../types'; import { getSpanKind } from './getSpanKind'; @@ -45,10 +50,7 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp } // if http.method exists, this is an http request span - // - // TODO: Referencing `http.request.method` is a temporary workaround until the semantic - // conventions export an attribute key for it. - const httpMethod = attributes['http.request.method'] || attributes[SEMATTRS_HTTP_METHOD]; + const httpMethod = attributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD]; if (httpMethod) { return descriptionForHttpMethod({ attributes, name, kind }, httpMethod); } @@ -213,7 +215,7 @@ export function getSanitizedUrl( // This is the relative path of the URL, e.g. /sub const httpTarget = attributes[SEMATTRS_HTTP_TARGET]; // This is the full URL, including host & query params etc., e.g. https://example.com/sub?foo=bar - const httpUrl = attributes[SEMATTRS_HTTP_URL]; + const httpUrl = attributes[SEMATTRS_HTTP_URL] || attributes[SEMANTIC_ATTRIBUTE_URL_FULL]; // This is the normalized route name - may not always be available! const httpRoute = attributes[SEMATTRS_HTTP_ROUTE]; diff --git a/yarn.lock b/yarn.lock index 246bb480abd9..edab1265fe2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7242,6 +7242,14 @@ "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" +"@opentelemetry/instrumentation-undici@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz#50782ff300027d0d0664fb60a3c12227586d5ebd" + integrity sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww== + dependencies: + "@opentelemetry/core" "^1.8.0" + "@opentelemetry/instrumentation" "^0.52.0" + "@opentelemetry/instrumentation@0.53.0", "@opentelemetry/instrumentation@^0.53.0": version "0.53.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz#e6369e4015eb5112468a4d45d38dcada7dad892d" @@ -7265,18 +7273,7 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.46.0": - version "0.46.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.46.0.tgz#a8a252306f82e2eace489312798592a14eb9830e" - integrity sha512-a9TijXZZbk0vI5TGLZl+0kxyFfrXHhX6Svtz7Pp2/VBlCSKrazuULEyoJQrOknJyFWNMEmbbJgOciHCCpQcisw== - dependencies: - "@types/shimmer" "^1.0.2" - import-in-the-middle "1.7.1" - require-in-the-middle "^7.1.1" - semver "^7.5.2" - shimmer "^1.2.1" - -"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.1": +"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.0", "@opentelemetry/instrumentation@^0.52.1": version "0.52.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== @@ -20661,16 +20658,6 @@ import-in-the-middle@1.4.2: cjs-module-lexer "^1.2.2" module-details-from-path "^1.0.3" -import-in-the-middle@1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.1.tgz#3e111ff79c639d0bde459bd7ba29dd9fdf357364" - integrity sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg== - dependencies: - acorn "^8.8.2" - acorn-import-assertions "^1.9.0" - cjs-module-lexer "^1.2.2" - module-details-from-path "^1.0.3" - import-in-the-middle@^1.11.0, import-in-the-middle@^1.8.1: version "1.11.0" resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708" @@ -26192,14 +26179,6 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -opentelemetry-instrumentation-fetch-node@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.3.tgz#beb24048bdccb1943ba2a5bbadca68020e448ea7" - integrity sha512-Qb11T7KvoCevMaSeuamcLsAD+pZnavkhDnlVL0kRozfhl42dKG5Q3anUklAFKJZjY3twLR+BnRa6DlwwkIE/+A== - dependencies: - "@opentelemetry/instrumentation" "^0.46.0" - "@opentelemetry/semantic-conventions" "^1.17.0" - opentelemetry-instrumentation-remix@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-remix/-/opentelemetry-instrumentation-remix-0.7.1.tgz#ef90ede718612786f7015e5496bd25cac8c49ce3" From a7d3a9d4075a200018f3c635f053922759a68469 Mon Sep 17 00:00:00 2001 From: Kaung Zin Hein <83657429+Zen-cronic@users.noreply.github.com> Date: Mon, 9 Sep 2024 04:34:34 -0400 Subject: [PATCH 02/22] fix(vue): Ensure Vue `trackComponents` list matches components with or without `<>` (#13543) Ensure that the component names listed in the `trackComponent` option match regardless of if they were specified as `` or `Name`. Add unit and e2e tests for component tracking. --------- Signed-off-by: Kaung Zin Hein --- .../test-applications/vue-3/src/main.ts | 1 + .../vue-3/src/router/index.ts | 4 + .../vue-3/src/views/ComponentMainView.vue | 10 +++ .../vue-3/src/views/ComponentOneView.vue | 10 +++ .../vue-3/src/views/ComponentTwoView.vue | 10 +++ .../vue-3/tests/performance.test.ts | 83 +++++++++++++++++++ packages/vue/src/tracing.ts | 18 +++- .../vue/test/tracing/trackComponents.test.ts | 44 ++++++++++ 8 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentMainView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentOneView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentTwoView.vue create mode 100644 packages/vue/test/tracing/trackComponents.test.ts diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts index f4a01d4285c5..13064ce04080 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts @@ -19,6 +19,7 @@ Sentry.init({ }), ], tunnel: `http://localhost:3031/`, // proxy server + trackComponents: ['ComponentMainView', ''], }); app.use(router); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts index aac6fb815f43..3a05e4f1055a 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts @@ -30,6 +30,10 @@ const router = createRouter({ }, ], }, + { + path: '/components', + component: () => import('../views/ComponentMainView.vue'), + }, ], }); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentMainView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentMainView.vue new file mode 100644 index 000000000000..69f60c769f54 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentMainView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentOneView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentOneView.vue new file mode 100644 index 000000000000..02c5d133ffb3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentOneView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentTwoView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentTwoView.vue new file mode 100644 index 000000000000..f5a1c3c5f46d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentTwoView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts index d9a594b5abe7..03c2cdbb01e5 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts @@ -122,3 +122,86 @@ test('sends a pageload transaction with a route name as transaction name if avai }, }); }); + +test('sends a lifecycle span for each tracked components', async ({ page }) => { + const transactionPromise = waitForTransaction('vue-3', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/components`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + spans: expect.arrayContaining([ + // enabled by default + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.render', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Application Render', + op: 'ui.vue.render', + origin: 'auto.ui.vue', + }), + // enabled by default + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + + // without `<>` + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + + // with `<>` + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + + // not tracked + expect.not.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + ]), + transaction: '/components', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 135b7fa8c9cc..3085e528bbf0 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -46,6 +46,19 @@ function finishRootSpan(vm: VueSentry, timestamp: number, timeout: number): void }, timeout); } +/** Find if the current component exists in the provided `TracingOptions.trackComponents` array option. */ +export function findTrackComponent(trackComponents: string[], formattedName: string): boolean { + function extractComponentName(name: string): string { + return name.replace(/^<([^\s]*)>(?: at [^\s]*)?$/, '$1'); + } + + const isMatched = trackComponents.some(compo => { + return extractComponentName(formattedName) === extractComponentName(compo); + }); + + return isMatched; +} + export const createTracingMixins = (options: TracingOptions): Mixins => { const hooks = (options.hooks || []) .concat(DEFAULT_HOOKS) @@ -84,8 +97,9 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // Skip components that we don't want to track to minimize the noise and give a more granular control to the user const name = formatComponentName(this, false); + const shouldTrack = Array.isArray(options.trackComponents) - ? options.trackComponents.indexOf(name) > -1 + ? findTrackComponent(options.trackComponents, name) : options.trackComponents; // We always want to track root component @@ -109,7 +123,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { } this.$_sentrySpans[operation] = startInactiveSpan({ - name: `Vue <${name}>`, + name: `Vue ${name}`, op: `${VUE_OP}.${operation}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', diff --git a/packages/vue/test/tracing/trackComponents.test.ts b/packages/vue/test/tracing/trackComponents.test.ts new file mode 100644 index 000000000000..9115522a3033 --- /dev/null +++ b/packages/vue/test/tracing/trackComponents.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { findTrackComponent } from '../../src/tracing'; + +describe('findTrackComponent', () => { + describe('when user-defined array contains ``', () => { + it('returns true if a match is found', () => { + // arrange + const trackComponents = ['', '']; + const formattedComponentName = ''; + + // act + const shouldTrack = findTrackComponent(trackComponents, formattedComponentName); + + // assert + expect(shouldTrack).toBe(true); + }); + }); + describe('when user-defined array contains `Component` without the `<>`', () => { + it('returns true if a match is found', () => { + // arrange + const trackComponents = ['ABC', 'XYZ']; + const formattedComponentName = ''; + + // act + const shouldTrack = findTrackComponent(trackComponents, formattedComponentName); + + // assert + expect(shouldTrack).toBe(true); + }); + }); + describe('when the vue file name is include in the formatted component name', () => { + it('returns true if a match is found', () => { + // arrange + const trackComponents = ['ABC', 'XYZ']; + const formattedComponentName = ' at XYZ.vue'; + + // act + const shouldTrack = findTrackComponent(trackComponents, formattedComponentName); + + // assert + expect(shouldTrack).toBe(true); + }); + }); +}); From 4e9533e765b973cf758e6ee12bf86a4181c785c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:26:39 +0000 Subject: [PATCH 03/22] ref: Add external contributor to CHANGELOG.md (#13625) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #13543 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b0f4361b92b..bf62e68f0f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @Zen-cronic. Thank you for your contribution! + ## 8.29.0 ### Important Changes From 15dd865ab5493dd036ef42965b1d981a935f7f2e Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Sep 2024 11:28:20 +0200 Subject: [PATCH 04/22] test(node): Use fake timers to speed up unit tests (#13607) Changes the rate-limiting tests to use fake timers. This takes the test time from 24s to 4s. --- .../test/integrations/localvariables.test.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index db9385214d42..ad9f6804d9a3 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -2,7 +2,7 @@ import { createRateLimiter } from '../../src/integrations/local-variables/common import { createCallbackList } from '../../src/integrations/local-variables/local-variables-sync'; import { NODE_MAJOR } from '../../src/nodeVersion'; -jest.setTimeout(20_000); +jest.useFakeTimers(); const describeIf = (condition: boolean) => (condition ? describe : describe.skip); @@ -87,10 +87,13 @@ describeIf(NODE_MAJOR >= 18)('LocalVariables', () => { for (let i = 0; i < 7; i++) { increment(); + jest.advanceTimersByTime(100); } + + jest.advanceTimersByTime(1_000); }); - it('does not call disable if not exceeded', done => { + it('does not call disable if not exceeded', () => { const increment = createRateLimiter( 5, () => { @@ -101,20 +104,17 @@ describeIf(NODE_MAJOR >= 18)('LocalVariables', () => { }, ); - let count = 0; - - const timer = setInterval(() => { - for (let i = 0; i < 4; i++) { - increment(); - } + for (let i = 0; i < 4; i++) { + increment(); + jest.advanceTimersByTime(200); + } - count += 1; + jest.advanceTimersByTime(600); - if (count >= 5) { - clearInterval(timer); - done(); - } - }, 1_000); + for (let i = 0; i < 4; i++) { + increment(); + jest.advanceTimersByTime(200); + } }); it('re-enables after timeout', done => { @@ -134,7 +134,10 @@ describeIf(NODE_MAJOR >= 18)('LocalVariables', () => { for (let i = 0; i < 10; i++) { increment(); + jest.advanceTimersByTime(100); } + + jest.advanceTimersByTime(10_000); }); }); }); From c7f50a792c0f95031182a0451b33c593fe62b9ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:50:52 +0000 Subject: [PATCH 05/22] feat(deps): Bump @opentelemetry/instrumentation-undici from 0.5.0 to 0.6.0 (#13622) --- packages/node/package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 2eb752807ef0..a77c1b56b215 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -86,7 +86,7 @@ "@opentelemetry/instrumentation-nestjs-core": "0.40.0", "@opentelemetry/instrumentation-pg": "0.44.0", "@opentelemetry/instrumentation-redis-4": "0.42.0", - "@opentelemetry/instrumentation-undici": "0.5.0", + "@opentelemetry/instrumentation-undici": "0.6.0", "@opentelemetry/resources": "^1.25.1", "@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.25.1", diff --git a/yarn.lock b/yarn.lock index edab1265fe2c..1a57c960cfcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7047,14 +7047,14 @@ dependencies: "@opentelemetry/semantic-conventions" "1.25.0" -"@opentelemetry/core@1.25.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.23.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0": +"@opentelemetry/core@1.25.1": version "1.25.1" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.1.tgz#ff667d939d128adfc7c793edae2f6bca177f829d" integrity sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ== dependencies: "@opentelemetry/semantic-conventions" "1.25.1" -"@opentelemetry/core@1.26.0": +"@opentelemetry/core@1.26.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.23.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0": version "1.26.0" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.26.0.tgz#7d84265aaa850ed0ca5813f97d831155be42b328" integrity sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ== @@ -7242,13 +7242,13 @@ "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-undici@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz#50782ff300027d0d0664fb60a3c12227586d5ebd" - integrity sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww== +"@opentelemetry/instrumentation-undici@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz#9436ee155c8dcb0b760b66947c0e0f347688a5ef" + integrity sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.52.0" + "@opentelemetry/instrumentation" "^0.53.0" "@opentelemetry/instrumentation@0.53.0", "@opentelemetry/instrumentation@^0.53.0": version "0.53.0" @@ -7273,7 +7273,7 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.0", "@opentelemetry/instrumentation@^0.52.1": +"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.1": version "0.52.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== From ce37ffeafce9e3a55b63718747c5d541311c89db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:51:08 +0000 Subject: [PATCH 06/22] ci(deps): Bump denoland/setup-deno from 1.4.0 to 1.4.1 (#13621) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70535bd06e89..626955ac2718 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -437,7 +437,7 @@ jobs: with: node-version-file: 'package.json' - name: Set up Deno - uses: denoland/setup-deno@1.4.0 + uses: denoland/setup-deno@v1.4.1 with: deno-version: v1.38.5 - name: Restore caches From 44f3ffab675f20d99d48afc57a94324975c8483f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:58:44 +0000 Subject: [PATCH 07/22] feat(deps): Bump @sentry/cli from 2.33.0 to 2.35.0 (#13624) --- packages/remix/package.json | 2 +- yarn.lock | 146 ++++++++++++------------------------ 2 files changed, 47 insertions(+), 101 deletions(-) diff --git a/packages/remix/package.json b/packages/remix/package.json index 8ffb03176e6e..3e9151629322 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -54,7 +54,7 @@ "dependencies": { "@opentelemetry/instrumentation-http": "0.53.0", "@remix-run/router": "1.x", - "@sentry/cli": "^2.33.0", + "@sentry/cli": "^2.35.0", "@sentry/core": "8.29.0", "@sentry/node": "8.29.0", "@sentry/opentelemetry": "8.29.0", diff --git a/yarn.lock b/yarn.lock index 1a57c960cfcf..86f0c70cbdbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8229,80 +8229,45 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.33.0.tgz#c0f3352a9e58e4f02deca52f0d5a9bd14b3e4a32" - integrity sha512-LQFvD7uCOQ2P/vYru7IBKqJDHwJ9Rr2vqqkdjbxe2YCQS/N3NPXvi3eVM9hDJ284oyV/BMZ5lrmVTuIicf/hhw== - -"@sentry/cli-darwin@2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.33.1.tgz#e4eb1dd01ee3ce2788025426b860ccc63759589c" - integrity sha512-+4/VIx/E1L2hChj5nGf5MHyEPHUNHJ/HoG5RY+B+vyEutGily1c1+DM2bum7RbD0xs6wKLIyup5F02guzSzG8A== - -"@sentry/cli-linux-arm64@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.33.0.tgz#14bc2556aa1011b96e7964756f84c4215a087ea7" - integrity sha512-mR2ZhqpU8RBVGLF5Ji19iOmVznk1B7Bzg5VhA8bVPuKsQmFN/3SyqE87IPMhwKoAsSRXyctwmbAkKs4240fxGA== - -"@sentry/cli-linux-arm64@2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.33.1.tgz#9ea1718c21ef32ca83b0852ca29fb461fd26d25a" - integrity sha512-DbGV56PRKOLsAZJX27Jt2uZ11QfQEMmWB4cIvxkKcFVE+LJP4MVA+MGGRUL6p+Bs1R9ZUuGbpKGtj0JiG6CoXw== - -"@sentry/cli-linux-arm@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.33.0.tgz#e00f9698b6c79e064490a32d11ad7d1909a15314" - integrity sha512-gY1bFE7wjDJc7WiNq1AS0WrILqLLJUw6Ou4pFQS45KjaH3/XJ1eohHhGJNy/UBHJ/Gq32b/BA9vsnWTXClZJ7g== - -"@sentry/cli-linux-arm@2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.33.1.tgz#e8a1dca4d008dd6a72ab5935304c104e98e2901c" - integrity sha512-zbxEvQju+tgNvzTOt635le4kS/Fbm2XC2RtYbCTs034Vb8xjrAxLnK0z1bQnStUV8BkeBHtsNVrG+NSQDym2wg== - -"@sentry/cli-linux-i686@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.33.0.tgz#f2475caa9897067f25114aa368e6b3ac11c86652" - integrity sha512-XPIy0XpqgAposHtWsy58qsX85QnZ8q0ktBuT4skrsCrLMzfhoQg4Ua+YbUr3RvE814Rt8Hzowx2ar2Rl3pyCyw== - -"@sentry/cli-linux-i686@2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.33.1.tgz#f1fe8dd4d6dde0812a94fba31de8054ddfb7284a" - integrity sha512-g2LS4oPXkPWOfKWukKzYp4FnXVRRSwBxhuQ9eSw2peeb58ZIObr4YKGOA/8HJRGkooBJIKGaAR2mH2Pk1TKaiA== - -"@sentry/cli-linux-x64@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.33.0.tgz#181936a6f37dd237a2f867c11244b26e2d58d5fa" - integrity sha512-qe1DdCUv4tmqS03s8RtCkEX9vCW2G+NgOxX6jZ5jN/sKDwjUlquljqo7JHUGSupkoXmymnNPm5By3rNr6VyNHg== - -"@sentry/cli-linux-x64@2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.33.1.tgz#6e086675356a9eb79731bf9e447d078bae1b5adf" - integrity sha512-IV3dcYV/ZcvO+VGu9U6kuxSdbsV2kzxaBwWUQxtzxJ+cOa7J8Hn1t0koKGtU53JVZNBa06qJWIcqgl4/pCuKIg== - -"@sentry/cli-win32-i686@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.33.0.tgz#3ab02ea0ef159a801701d41e0a16f52d4e751cdb" - integrity sha512-VEXWtJ69C3b+kuSmXQJRwdQ0ypPGH88hpqyQuosbAOIqh/sv4g9B/u1ETHZc+whLdFDpPcTLVMbLDbXTGug0Yg== - -"@sentry/cli-win32-i686@2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.33.1.tgz#0e6b36c4a2f5f6e85a59247a123d276b3ef10f1a" - integrity sha512-F7cJySvkpzIu7fnLKNHYwBzZYYwlhoDbAUnaFX0UZCN+5DNp/5LwTp37a5TWOsmCaHMZT4i9IO4SIsnNw16/zQ== - -"@sentry/cli-win32-x64@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.0.tgz#fc9ec9b7cbec80d7cd39aaa570b7682399a0b1de" - integrity sha512-GIUKysZ1xbSklY9h1aVaLMSYLsnMSd+JuwQLR+0wKw2wJC4O5kNCPFSGikhiOZM/kvh3GO1WnXNyazFp8nLAzw== - -"@sentry/cli-win32-x64@2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.1.tgz#2d00b38a2dd9f3355df91825582ada3ea0034e86" - integrity sha512-8VyRoJqtb2uQ8/bFRKNuACYZt7r+Xx0k2wXRGTyH05lCjAiVIXn7DiS2BxHFty7M1QEWUCMNsb/UC/x/Cu2wuA== - -"@sentry/cli@^2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.33.0.tgz#5de59f829070ab20d360fae25924f39c55afd8ba" - integrity sha512-9MOzQy1UunVBhPOfEuO0JH2ofWAMmZVavTTR/Bo2CkJwI1qjyVF0UKLTXE3l4ujiJnFufOoBsVyKmYWXFerbCw== +"@sentry/cli-darwin@2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.35.0.tgz#4bc9a07690f0de75d930ba47f4655f6465191768" + integrity sha512-dRtDaASkB1ncSbCLMIL8bxki4dPMimSdYz74XOUJ5IvDVVzEInEO7PqvyOj/cyafB+1FSNudaZ90ZRvsNN1Maw== + +"@sentry/cli-linux-arm64@2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.35.0.tgz#bad8a45b81d2b317f702991783a503f566b2294e" + integrity sha512-NpyVz2lQWWkMa9GZkt0m4cA/wsgYnWOE6Z+4ePUGjbOIG3Ws9DLaHjYxUUYI79kxfbVCp7wLo1S6kOkj+M1Dlw== + +"@sentry/cli-linux-arm@2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.35.0.tgz#dacfc219876f5dce3d8c65dab7128ea3e493f561" + integrity sha512-zNL+/HnepZ4/MkIS8wfoUQxSa+k6r0DSSdX1TpDH5436u+3LB5rfCTBfZ624DWHKMoXX+1dI+rWSi+zL8QFMsg== + +"@sentry/cli-linux-i686@2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.35.0.tgz#d0e6401b60b0a4b6c3578998995ba6cb31c1bf20" + integrity sha512-vIYwZVqx+kYZdPsenIm+UqjSCKe9Q2Aof6kzrzW0DPR1WyqIWbWG4NbiugiPTiuA1dLjUjYpGP8wyIqb8hxv4w== + +"@sentry/cli-linux-x64@2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.35.0.tgz#a1e8e7bff960ed8916b4cc9c0ef75a057e30f989" + integrity sha512-7Wy5QNt6wZ8EaxEbHqP0DEiyUcXRVItRt9jzhpa2nCaawL+fwDOQCjUkHGsdIC+y14UqA+er9CaPCSp8sA6Vaw== + +"@sentry/cli-win32-i686@2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.35.0.tgz#c1b090f7c740c5b22d1019ca48a84f58cd4b2670" + integrity sha512-XDcBUtO5A9elH+xgFNs6NBjkMBnz0sZLo5DU7LE77qKXULnlLeJ63eZD1ukQIRPvxEDsIEPOllRweLuAlUMDtw== + +"@sentry/cli-win32-x64@2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.35.0.tgz#f592af483da239be846e556f57f5c6fc7dc1dc54" + integrity sha512-86yHO+31qAXUeAdSCH7MNodn/cn/9xd2fTrxjtfNZWO0pX0jW91sCdomfBxhu5b977cyV9gNcqeBbc9XSIKIIA== + +"@sentry/cli@^2.33.1", "@sentry/cli@^2.35.0": + version "2.35.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.35.0.tgz#5514eb8f5808bc70707ffa186156f8ff7ca5971e" + integrity sha512-7sHRJViEgHTfEXf+HD1Fb2cwmnxlILmb2NNxghP2vvrgC2PhuwuJU7AX4zg7HjJgxH9HBmnn4AJskDujaJ/6cQ== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -8310,32 +8275,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.33.0" - "@sentry/cli-linux-arm" "2.33.0" - "@sentry/cli-linux-arm64" "2.33.0" - "@sentry/cli-linux-i686" "2.33.0" - "@sentry/cli-linux-x64" "2.33.0" - "@sentry/cli-win32-i686" "2.33.0" - "@sentry/cli-win32-x64" "2.33.0" - -"@sentry/cli@^2.33.1": - version "2.33.1" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.33.1.tgz#cfbdffdd896b05b92a659baf435b5607037af928" - integrity sha512-dUlZ4EFh98VFRPJ+f6OW3JEYQ7VvqGNMa0AMcmvk07ePNeK/GicAWmSQE4ZfJTTl80ul6HZw1kY01fGQOQlVRA== - dependencies: - https-proxy-agent "^5.0.0" - node-fetch "^2.6.7" - progress "^2.0.3" - proxy-from-env "^1.1.0" - which "^2.0.2" - optionalDependencies: - "@sentry/cli-darwin" "2.33.1" - "@sentry/cli-linux-arm" "2.33.1" - "@sentry/cli-linux-arm64" "2.33.1" - "@sentry/cli-linux-i686" "2.33.1" - "@sentry/cli-linux-x64" "2.33.1" - "@sentry/cli-win32-i686" "2.33.1" - "@sentry/cli-win32-x64" "2.33.1" + "@sentry/cli-darwin" "2.35.0" + "@sentry/cli-linux-arm" "2.35.0" + "@sentry/cli-linux-arm64" "2.35.0" + "@sentry/cli-linux-i686" "2.35.0" + "@sentry/cli-linux-x64" "2.35.0" + "@sentry/cli-win32-i686" "2.35.0" + "@sentry/cli-win32-x64" "2.35.0" "@sentry/rollup-plugin@2.22.3": version "2.22.3" From 45156d2fd383711c5e20ca5427b9fed5e69179ee Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:18:23 +0200 Subject: [PATCH 08/22] feat(nuxt): Add server config to root folder (#13583) > This is a draft PR as this approach leads to an error as `hook.mjs` is not included in the `node_modules`. This has been fixed upstream but was not yet released for nuxt. Makes it possible to include a `sentry.server.config.ts` file in the root folder alongside `sentry.client.config.ts`. Currently, it has to be added in the `public` folder which is not 100% ideal. --- .../test-applications/nuxt-3/package.json | 4 +- packages/nuxt/src/module.ts | 31 ++++------ packages/nuxt/src/vite/addServerConfig.ts | 57 +++++++++++++++++ packages/nuxt/src/vite/utils.ts | 28 +++++++++ packages/nuxt/test/vite/utils.test.ts | 61 +++++++++++++++++++ 5 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 packages/nuxt/src/vite/addServerConfig.ts create mode 100644 packages/nuxt/src/vite/utils.ts create mode 100644 packages/nuxt/test/vite/utils.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index 93471fff7aab..7173aeaa4ce5 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -14,10 +14,10 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "3.12.4" + "nuxt": "3.13.1" }, "devDependencies": { - "@nuxt/test-utils": "^3.13.1", + "@nuxt/test-utils": "^3.14.1", "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils" } diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 5d529c99330c..faa48e5c3c26 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -1,8 +1,8 @@ -import * as fs from 'fs'; -import * as path from 'path'; import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'; import type { SentryNuxtModuleOptions } from './common/types'; +import { addServerConfigToBuild } from './vite/addServerConfig'; import { setupSourceMaps } from './vite/sourceMaps'; +import { findDefaultSdkInitFile } from './vite/utils'; export type ModuleOptions = SentryNuxtModuleOptions; @@ -62,22 +62,15 @@ export default defineNuxtModule({ if (clientConfigFile || serverConfigFile) { setupSourceMaps(moduleOptions, nuxt); } + if (serverConfigFile && serverConfigFile.includes('.server.config')) { + if (moduleOptions.debug) { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Using your \`${serverConfigFile}\` file for the server-side Sentry configuration. In case you have a \`public/instrument.server\` file, the \`public/instrument.server\` file will be ignored. Make sure the file path in your node \`--import\` option matches the Sentry server config file in your \`.output\` folder and has a \`.mjs\` extension.`, + ); + } + + addServerConfigToBuild(moduleOptions, nuxt, serverConfigFile); + } }, }); - -function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined { - const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts']; - - const cwd = process.cwd(); - const filePath = possibleFileExtensions - .map(e => - path.resolve( - type === 'server' - ? path.join(cwd, 'public', `instrument.${type}.${e}`) - : path.join(cwd, `sentry.${type}.config.${e}`), - ), - ) - .find(filename => fs.existsSync(filename)); - - return filePath ? path.basename(filePath) : undefined; -} diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts new file mode 100644 index 000000000000..dc1fc21dd6bd --- /dev/null +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -0,0 +1,57 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { createResolver } from '@nuxt/kit'; +import type { Nuxt } from '@nuxt/schema'; +import type { SentryNuxtModuleOptions } from '../common/types'; + +/** + * Adds the `sentry.server.config.ts` file as `sentry.server.config.mjs` to the `.output` directory to be able to reference this file in the node --import option. + * + * 1. Adding the file as a rollup import, so it is included in the build (automatically transpiles the file). + * 2. Copying the file to the `.output` directory after the build process is finished. + */ +export function addServerConfigToBuild( + moduleOptions: SentryNuxtModuleOptions, + nuxt: Nuxt, + serverConfigFile: string, +): void { + nuxt.hook('vite:extendConfig', async (viteInlineConfig, _env) => { + if ( + typeof viteInlineConfig?.build?.rollupOptions?.input === 'object' && + 'server' in viteInlineConfig.build.rollupOptions.input + ) { + // Create a rollup entry for the server config to add it as `sentry.server.config.mjs` to the build + (viteInlineConfig.build.rollupOptions.input as { [entryName: string]: string })['sentry.server.config'] = + createResolver(nuxt.options.srcDir).resolve(`/${serverConfigFile}`); + } + + /** + * When the build process is finished, copy the `sentry.server.config` file to the `.output` directory. + * This is necessary because we need to reference this file path in the node --import option. + */ + nuxt.hook('close', async () => { + const source = path.resolve('.nuxt/dist/server/sentry.server.config.mjs'); + const destination = path.resolve('.output/server/sentry.server.config.mjs'); + + try { + await fs.promises.access(source, fs.constants.F_OK); + await fs.promises.copyFile(source, destination); + + if (moduleOptions.debug) { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Successfully added the content of the \`${serverConfigFile}\` file to \`${destination}\``, + ); + } + } catch (error) { + if (moduleOptions.debug) { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] An error occurred when trying to add the \`${serverConfigFile}\` file to the \`.output\` directory`, + error, + ); + } + } + }); + }); +} diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts new file mode 100644 index 000000000000..7d794e807fd7 --- /dev/null +++ b/packages/nuxt/src/vite/utils.ts @@ -0,0 +1,28 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Find the default SDK init file for the given type (client or server). + * The sentry.server.config file is prioritized over the instrument.server file. + */ +export function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined { + const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts']; + const cwd = process.cwd(); + + const filePaths: string[] = []; + if (type === 'server') { + for (const ext of possibleFileExtensions) { + // order is important here - we want to prioritize the server.config file + filePaths.push(path.join(cwd, `sentry.${type}.config.${ext}`)); + filePaths.push(path.join(cwd, 'public', `instrument.${type}.${ext}`)); + } + } else { + for (const ext of possibleFileExtensions) { + filePaths.push(path.join(cwd, `sentry.${type}.config.${ext}`)); + } + } + + const filePath = filePaths.find(filename => fs.existsSync(filename)); + + return filePath ? path.basename(filePath) : undefined; +} diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts new file mode 100644 index 000000000000..0ca81b3e2986 --- /dev/null +++ b/packages/nuxt/test/vite/utils.test.ts @@ -0,0 +1,61 @@ +import * as fs from 'fs'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { findDefaultSdkInitFile } from '../../src/vite/utils'; + +vi.mock('fs'); + +describe('findDefaultSdkInitFile', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( + 'should return the server file with .%s extension if it exists', + ext => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.includes(`sentry.server.config.${ext}`); + }); + + const result = findDefaultSdkInitFile('server'); + expect(result).toBe(`sentry.server.config.${ext}`); + }, + ); + + it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( + 'should return the client file with .%s extension if it exists', + ext => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.includes(`sentry.client.config.${ext}`); + }); + + const result = findDefaultSdkInitFile('client'); + expect(result).toBe(`sentry.client.config.${ext}`); + }, + ); + + it('should return undefined if no file with specified extensions exists', () => { + vi.spyOn(fs, 'existsSync').mockReturnValue(false); + + const result = findDefaultSdkInitFile('server'); + expect(result).toBeUndefined(); + }); + + it('should return undefined if no file exists', () => { + vi.spyOn(fs, 'existsSync').mockReturnValue(false); + + const result = findDefaultSdkInitFile('server'); + expect(result).toBeUndefined(); + }); + + it('should return the server config file if server.config and instrument exist', () => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return ( + !(filePath instanceof URL) && + (filePath.includes('sentry.server.config.js') || filePath.includes('instrument.server.js')) + ); + }); + + const result = findDefaultSdkInitFile('server'); + expect(result).toBe('sentry.server.config.js'); + }); +}); From 8eaa5628ee085b0172869145f58ed5f69a16ee3e Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Sep 2024 12:40:34 +0200 Subject: [PATCH 09/22] feat(core): Allow adding measurements without global client (#13612) While working on updating [`sentry-javascript-bundler-plugins` to use v8 of the JavaScript SDK](https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/579), I found that I was unable to set measurements as the global client is not used. If you're not using a global client, there is currently no way to add measurements because `Sentry.setMeasurement()` relies on `getActiveSpan()` which in turn relies on `getCurrentScope()`. This PR moves the `activeSpan` into the last parameter which defaults to `getActiveSpan()`. --- packages/core/src/tracing/measurement.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index 817569a03930..c8802023bf2e 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -6,10 +6,10 @@ import { import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; /** - * Adds a measurement to the current active transaction. + * Adds a measurement to the active transaction on the current global scope. You can optionally pass in a different span + * as the 4th parameter. */ -export function setMeasurement(name: string, value: number, unit: MeasurementUnit): void { - const activeSpan = getActiveSpan(); +export function setMeasurement(name: string, value: number, unit: MeasurementUnit, activeSpan = getActiveSpan()): void { const rootSpan = activeSpan && getRootSpan(activeSpan); if (rootSpan) { From 315a5db44db1ff5d1672b336a4847b56a4f9def9 Mon Sep 17 00:00:00 2001 From: odanado Date: Mon, 9 Sep 2024 20:45:17 +0900 Subject: [PATCH 10/22] fix(browser): check supportedEntryTypes before caling the function (#13541) `PerformanceObserver` is available on iOS 11 and later, but the `supportedEntryTypes` method is available on iOS 13 and later. ref: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver Therefore, a runtime error will occur if we use Sentry on iOS 11 or iOS 12. --- packages/browser/src/tracing/browserTracingIntegration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index ff5201878cff..523edd7e4262 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -218,6 +218,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Mon, 9 Sep 2024 12:01:37 +0000 Subject: [PATCH 11/22] ref: Add external contributor to CHANGELOG.md (#13630) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #13541 Co-authored-by: AbhiPrasad <18689448+AbhiPrasad@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf62e68f0f5f..bc50808b4f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @Zen-cronic. Thank you for your contribution! +Work in this release was contributed by @Zen-cronic and @odanado. Thank you for your contributions! ## 8.29.0 From 0d79b51e7ecbe26b1b235ed92410d2d07febdcde Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:20:10 +0200 Subject: [PATCH 12/22] fix(node): Update OpenTelemetry instrumentation package for solidstart and opentelemetry (#13640) Looks like we overlooked two packages when updating deps previously in #13587. See: https://github.com/getsentry/sentry-javascript/pull/13587#issuecomment-2339419127 Closes: #13219 --- packages/opentelemetry/package.json | 2 +- packages/solidstart/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 99fd7e84a6c8..688c5e01f2b6 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -46,7 +46,7 @@ "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.25.1", - "@opentelemetry/instrumentation": "^0.52.1", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.25.1" }, diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index bfbafd7a0232..3a56d6d2b9e7 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -66,7 +66,7 @@ } }, "dependencies": { - "@opentelemetry/instrumentation": "^0.52.1", + "@opentelemetry/instrumentation": "^0.53.0", "@sentry/core": "8.29.0", "@sentry/node": "8.29.0", "@sentry/opentelemetry": "8.29.0", From 0c0c7f6a5e68fd1cb495d903adac663586be6ce5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 10 Sep 2024 09:28:33 +0200 Subject: [PATCH 13/22] fix: incorrect property name in `CHANGELOG.md` (#13635) I failed to update the description in my PR when we improved the property name after some discussion in the PR, so the wrong property name was used in the changelog: https://github.com/getsentry/sentry-javascript/blob/bcf571d9954094be76a99edbb12c23eff7f7b5dc/packages/node/src/types.ts#L20 Thanks to @torickjdavis for reporting this [here](https://github.com/getsentry/sentry-javascript/issues/12414#issuecomment-2339236336)! --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc50808b4f11..da1f91d499bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: '__PUBLIC_DSN__', - registerEsmLoaderHooks: { onlyHookedModules: true }, + registerEsmLoaderHooks: { onlyIncludeInstrumentedModules: true }, }); ``` From 703b5d4e42e6df70a5a676ab0d5b5ef2a38df1b0 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:37:08 +0200 Subject: [PATCH 14/22] fix(node): Update OpenTelemetry instrumentation package for solidstart and opentelemetry (#13642) Part 2 because I forgot to update the lockfile in the [previous PR](https://github.com/getsentry/sentry-javascript/pull/13640). --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 86f0c70cbdbd..69c732db9498 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7273,7 +7273,7 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0", "@opentelemetry/instrumentation@^0.52.1": +"@opentelemetry/instrumentation@^0.49 || ^0.50 || ^0.51 || ^0.52.0": version "0.52.1" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== From 7fa366ffc4ae849358747607d154eb36f67861ac Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 10 Sep 2024 05:05:07 -0400 Subject: [PATCH 15/22] ref(profiling): Conditionally shim cjs globals (#13267) The shims should only be applied if the globals are not present, else it results in double decl and a runtime error. The profiling SDK should gracefully handle env where the shims are already provided. I couldn't find a way to modify the shim as it is hardcoded in the plugin we are using so I went with the replace plugin approach and a placeholder value #poormansmacros. --- .github/workflows/build.yml | 2 +- .../node-profiling/build.shimmed.mjs | 29 +++++++++++++ .../node-profiling/package.json | 6 +-- packages/profiling-node/rollup.npm.config.mjs | 43 +++++++++++++++++-- packages/profiling-node/src/cpu_profiler.ts | 6 +++ 5 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 626955ac2718..38102ff204c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1168,7 +1168,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: 'dev-packages/e2e-tests/package.json' + node-version: 22 - name: Restore caches uses: ./.github/actions/restore-cache with: diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs b/dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs new file mode 100644 index 000000000000..c45e30539bc0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs @@ -0,0 +1,29 @@ +// Because bundlers can now predetermine a static set of binaries we need to ensure those binaries +// actually exists, else we risk a compile time error when bundling the package. This could happen +// if we added a new binary in cpu_profiler.ts, but forgot to prebuild binaries for it. Because CI +// only runs integration and unit tests, this change would be missed and could end up in a release. +// Therefor, once all binaries are precompiled in CI and tests pass, run esbuild with bundle:true +// which will copy all binaries to the outfile folder and throw if any of them are missing. +import esbuild from 'esbuild'; + +console.log('Running build using esbuild version', esbuild.version); + +esbuild.buildSync({ + platform: 'node', + entryPoints: ['./index.ts'], + outfile: './dist/index.shimmed.mjs', + target: 'esnext', + format: 'esm', + bundle: true, + loader: { '.node': 'copy' }, + banner: { + js: ` + import { dirname } from 'node:path'; + import { fileURLToPath } from 'node:url'; + import { createRequire } from 'node:module'; + const require = createRequire(import.meta.url); + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + `, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/package.json b/dev-packages/e2e-tests/test-applications/node-profiling/package.json index 94ec4926f2f6..a4c4bf1284fe 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling/package.json @@ -4,9 +4,9 @@ "private": true, "scripts": { "typecheck": "tsc --noEmit", - "build": "node build.mjs", - "test": "npm run build && node dist/index.js", - "clean": "npx rimraf node_modules", + "build": "node build.mjs && node build.shimmed.mjs", + "test": "node dist/index.js && node --experimental-require-module dist/index.js && node dist/index.shimmed.mjs", + "clean": "npx rimraf node_modules dist", "test:build": "npm run typecheck && npm run build", "test:assert": "npm run test" }, diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs index 1cc4f0936954..12492b7c83e8 100644 --- a/packages/profiling-node/rollup.npm.config.mjs +++ b/packages/profiling-node/rollup.npm.config.mjs @@ -1,12 +1,49 @@ import commonjs from '@rollup/plugin-commonjs'; -import esmshim from '@rollup/plugin-esm-shim'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants( +export const ESMShim = ` +import cjsUrl from 'node:url'; +import cjsPath from 'node:path'; +import cjsModule from 'node:module'; + +if(typeof __filename === 'undefined'){ + globalThis.__filename = cjsUrl.fileURLToPath(import.meta.url); +} + +if(typeof __dirname === 'undefined'){ + globalThis.__dirname = cjsPath.dirname(__filename); +} + +if(typeof require === 'undefined'){ + globalThis.require = cjsModule.createRequire(import.meta.url); +} +`; + +function makeESMShimPlugin(shim) { + return { + transform(code) { + const SHIM_REGEXP = /\/\/ #START_SENTRY_ESM_SHIM[\s\S]*?\/\/ #END_SENTRY_ESM_SHIM/; + return code.replace(SHIM_REGEXP, shim); + }, + }; +} + +const variants = makeNPMConfigVariants( makeBaseNPMConfig({ packageSpecificConfig: { output: { dir: 'lib', preserveModules: false }, - plugins: [commonjs(), esmshim()], + plugins: [commonjs()], }, }), ); + +for (const variant of variants) { + if (variant.output.format === 'esm') { + variant.plugins.push(makeESMShimPlugin(ESMShim)); + } else { + // Remove the ESM shim comment + variant.plugins.push(makeESMShimPlugin('')); + } +} + +export default variants; diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts index 9ab470e2ca70..fb739a939e77 100644 --- a/packages/profiling-node/src/cpu_profiler.ts +++ b/packages/profiling-node/src/cpu_profiler.ts @@ -15,6 +15,12 @@ import type { } from './types'; import type { ProfileFormat } from './types'; +// #START_SENTRY_ESM_SHIM +// When building for ESM, we shim require to use createRequire and __dirname. +// We need to do this because .node extensions in esm are not supported. +// The comment below this line exists as a placeholder for where to insert the shim. +// #END_SENTRY_ESM_SHIM + const stdlib = familySync(); const platform = process.env['BUILD_PLATFORM'] || _platform(); const arch = process.env['BUILD_ARCH'] || _arch(); From 4c6dd808f221b4f64e5505f1d545bdad1d0820e7 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 10 Sep 2024 11:08:00 +0200 Subject: [PATCH 16/22] fix(node): Remove ambiguity and race conditions when matching local variables to exceptions (#13501) Closes #13415 This PR only modifies the async version of this integration which is used for Node > v19. I tried applying similar changes to the sync integration and I cannot get it to work without causing memory leaks. @Bruno-DaSilva has been helping me explore different ways to fix a few fundamental issues with the local variables integration. Bruno found a way to [write to the error object](https://github.com/getsentry/sentry-javascript/issues/13415#issuecomment-2313754556) from the debugger which removes any ambiguity over which variables go with which exception. This allows us to remove the stack parsing and hashing which we were using previously to match up exceptions. Rather than write the `objectId` to the error, I have used this to write the entire local variables array directly to the error object. This completely negates the need to post the local variables from the worker thread which removes any possibility of race conditions. We then later pull the local variables directly from `hint.originalException.__SENTRY_ERROR_LOCAL_VARIABLES__`. --- .../integrations/local-variables/common.ts | 29 ++------- .../local-variables/inspector.d.ts | 8 +++ .../local-variables/local-variables-async.ts | 62 ++++++++----------- .../local-variables/local-variables-sync.ts | 26 +++++++- .../integrations/local-variables/worker.ts | 48 ++++++++------ 5 files changed, 93 insertions(+), 80 deletions(-) diff --git a/packages/node/src/integrations/local-variables/common.ts b/packages/node/src/integrations/local-variables/common.ts index 67c8d6d43d81..58ccea70d6de 100644 --- a/packages/node/src/integrations/local-variables/common.ts +++ b/packages/node/src/integrations/local-variables/common.ts @@ -1,10 +1,14 @@ import type { Debugger } from 'node:inspector'; -import type { StackFrame, StackParser } from '@sentry/types'; export type Variables = Record; export type RateLimitIncrement = () => void; +/** + * The key used to store the local variables on the error object. + */ +export const LOCAL_VARIABLES_KEY = '__SENTRY_ERROR_LOCAL_VARIABLES__'; + /** * Creates a rate limiter that will call the disable callback when the rate limit is reached and the enable callback * when a timeout has occurred. @@ -55,6 +59,7 @@ export type PausedExceptionEvent = Debugger.PausedEventDataType & { data: { // This contains error.stack description: string; + objectId?: string; }; }; @@ -68,28 +73,6 @@ export function functionNamesMatch(a: string | undefined, b: string | undefined) return a === b || (isAnonymous(a) && isAnonymous(b)); } -/** Creates a unique hash from stack frames */ -export function hashFrames(frames: StackFrame[] | undefined): string | undefined { - if (frames === undefined) { - return; - } - - // Only hash the 10 most recent frames (ie. the last 10) - return frames.slice(-10).reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, ''); -} - -/** - * We use the stack parser to create a unique hash from the exception stack trace - * This is used to lookup vars when the exception passes through the event processor - */ -export function hashFromStack(stackParser: StackParser, stack: string | undefined): string | undefined { - if (stack === undefined) { - return undefined; - } - - return hashFrames(stackParser(stack, 1)); -} - export interface FrameVariables { function: string; vars?: Variables; diff --git a/packages/node/src/integrations/local-variables/inspector.d.ts b/packages/node/src/integrations/local-variables/inspector.d.ts index 9ac6b857dcc0..5cfd496f7626 100644 --- a/packages/node/src/integrations/local-variables/inspector.d.ts +++ b/packages/node/src/integrations/local-variables/inspector.d.ts @@ -20,6 +20,14 @@ declare module 'node:inspector/promises' { method: 'Runtime.getProperties', params: Runtime.GetPropertiesParameterType, ): Promise; + public post( + method: 'Runtime.callFunctionOn', + params: Runtime.CallFunctionOnParameterType, + ): Promise; + public post( + method: 'Runtime.releaseObject', + params: Runtime.ReleaseObjectParameterType, + ): Promise; public on( event: 'Debugger.paused', diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts index 86ce9359a95e..d46fa224019f 100644 --- a/packages/node/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node/src/integrations/local-variables/local-variables-async.ts @@ -1,11 +1,12 @@ import { Worker } from 'node:worker_threads'; import { defineIntegration } from '@sentry/core'; -import type { Event, Exception, IntegrationFn } from '@sentry/types'; -import { LRUMap, logger } from '@sentry/utils'; +import type { Event, EventHint, Exception, IntegrationFn } from '@sentry/types'; +import { logger } from '@sentry/utils'; import type { NodeClient } from '../../sdk/client'; import type { FrameVariables, LocalVariablesIntegrationOptions, LocalVariablesWorkerArgs } from './common'; -import { functionNamesMatch, hashFrames } from './common'; +import { LOCAL_VARIABLES_KEY } from './common'; +import { functionNamesMatch } from './common'; // This string is a placeholder that gets overwritten with the worker code. export const base64WorkerScript = '###LocalVariablesWorkerScript###'; @@ -20,23 +21,7 @@ function log(...args: unknown[]): void { export const localVariablesAsyncIntegration = defineIntegration((( integrationOptions: LocalVariablesIntegrationOptions = {}, ) => { - const cachedFrames: LRUMap = new LRUMap(20); - - function addLocalVariablesToException(exception: Exception): void { - const hash = hashFrames(exception?.stacktrace?.frames); - - if (hash === undefined) { - return; - } - - // Check if we have local variables for an exception that matches the hash - // remove is identical to get but also removes the entry from the cache - const cachedFrame = cachedFrames.remove(hash); - - if (cachedFrame === undefined) { - return; - } - + function addLocalVariablesToException(exception: Exception, localVariables: FrameVariables[]): void { // Filter out frames where the function name is `new Promise` since these are in the error.stack frames // but do not appear in the debugger call frames const frames = (exception.stacktrace?.frames || []).filter(frame => frame.function !== 'new Promise'); @@ -45,32 +30,41 @@ export const localVariablesAsyncIntegration = defineIntegration((( // Sentry frames are in reverse order const frameIndex = frames.length - i - 1; - const cachedFrameVariable = cachedFrame[i]; - const frameVariable = frames[frameIndex]; + const frameLocalVariables = localVariables[i]; + const frame = frames[frameIndex]; - if (!frameVariable || !cachedFrameVariable) { + if (!frame || !frameLocalVariables) { // Drop out if we run out of frames to match up break; } if ( // We need to have vars to add - cachedFrameVariable.vars === undefined || + frameLocalVariables.vars === undefined || // We're not interested in frames that are not in_app because the vars are not relevant - frameVariable.in_app === false || + frame.in_app === false || // The function names need to match - !functionNamesMatch(frameVariable.function, cachedFrameVariable.function) + !functionNamesMatch(frame.function, frameLocalVariables.function) ) { continue; } - frameVariable.vars = cachedFrameVariable.vars; + frame.vars = frameLocalVariables.vars; } } - function addLocalVariablesToEvent(event: Event): Event { - for (const exception of event.exception?.values || []) { - addLocalVariablesToException(exception); + function addLocalVariablesToEvent(event: Event, hint: EventHint): Event { + if ( + hint.originalException && + typeof hint.originalException === 'object' && + LOCAL_VARIABLES_KEY in hint.originalException && + Array.isArray(hint.originalException[LOCAL_VARIABLES_KEY]) + ) { + for (const exception of event.exception?.values || []) { + addLocalVariablesToException(exception, hint.originalException[LOCAL_VARIABLES_KEY]); + } + + hint.originalException[LOCAL_VARIABLES_KEY] = undefined; } return event; @@ -96,10 +90,6 @@ export const localVariablesAsyncIntegration = defineIntegration((( worker.terminate(); }); - worker.on('message', ({ exceptionHash, frames }) => { - cachedFrames.set(exceptionHash, frames); - }); - worker.once('error', (err: Error) => { log('Worker error', err); }); @@ -139,8 +129,8 @@ export const localVariablesAsyncIntegration = defineIntegration((( }, ); }, - processEvent(event: Event): Event { - return addLocalVariablesToEvent(event); + processEvent(event: Event, hint: EventHint): Event { + return addLocalVariablesToEvent(event, hint); }, }; }) satisfies IntegrationFn); diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index bf7aaa26cbf3..d3203614330f 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -1,6 +1,6 @@ import type { Debugger, InspectorNotification, Runtime, Session } from 'node:inspector'; import { defineIntegration, getClient } from '@sentry/core'; -import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types'; +import type { Event, Exception, IntegrationFn, StackFrame, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; import { NODE_MAJOR } from '../../nodeVersion'; @@ -12,7 +12,29 @@ import type { RateLimitIncrement, Variables, } from './common'; -import { createRateLimiter, functionNamesMatch, hashFrames, hashFromStack } from './common'; +import { createRateLimiter, functionNamesMatch } from './common'; + +/** Creates a unique hash from stack frames */ +export function hashFrames(frames: StackFrame[] | undefined): string | undefined { + if (frames === undefined) { + return; + } + + // Only hash the 10 most recent frames (ie. the last 10) + return frames.slice(-10).reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, ''); +} + +/** + * We use the stack parser to create a unique hash from the exception stack trace + * This is used to lookup vars when the exception passes through the event processor + */ +export function hashFromStack(stackParser: StackParser, stack: string | undefined): string | undefined { + if (stack === undefined) { + return undefined; + } + + return hashFrames(stackParser(stack, 1)); +} type OnPauseEvent = InspectorNotification; export interface DebugSession { diff --git a/packages/node/src/integrations/local-variables/worker.ts b/packages/node/src/integrations/local-variables/worker.ts index 5bee22a84c29..eb4fee87947c 100644 --- a/packages/node/src/integrations/local-variables/worker.ts +++ b/packages/node/src/integrations/local-variables/worker.ts @@ -1,16 +1,12 @@ import type { Debugger, InspectorNotification, Runtime } from 'node:inspector'; import { Session } from 'node:inspector/promises'; -import { parentPort, workerData } from 'node:worker_threads'; -import type { StackParser } from '@sentry/types'; -import { createStackParser, nodeStackLineParser } from '@sentry/utils'; -import { createGetModuleFromFilename } from '../../utils/module'; +import { workerData } from 'node:worker_threads'; import type { LocalVariablesWorkerArgs, PausedExceptionEvent, RateLimitIncrement, Variables } from './common'; -import { createRateLimiter, hashFromStack } from './common'; +import { LOCAL_VARIABLES_KEY } from './common'; +import { createRateLimiter } from './common'; const options: LocalVariablesWorkerArgs = workerData; -const stackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename(options.basePath))); - function log(...args: unknown[]): void { if (options.debug) { // eslint-disable-next-line no-console @@ -88,19 +84,15 @@ let rateLimiter: RateLimitIncrement | undefined; async function handlePaused( session: Session, - stackParser: StackParser, - { reason, data, callFrames }: PausedExceptionEvent, -): Promise { + { reason, data: { objectId }, callFrames }: PausedExceptionEvent, +): Promise { if (reason !== 'exception' && reason !== 'promiseRejection') { return; } rateLimiter?.(); - // data.description contains the original error.stack - const exceptionHash = hashFromStack(stackParser, data?.description); - - if (exceptionHash == undefined) { + if (objectId == undefined) { return; } @@ -123,7 +115,15 @@ async function handlePaused( } } - parentPort?.postMessage({ exceptionHash, frames }); + // We write the local variables to a property on the error object. These can be read by the integration as the error + // event pass through the SDK event pipeline + await session.post('Runtime.callFunctionOn', { + functionDeclaration: `function() { this.${LOCAL_VARIABLES_KEY} = ${JSON.stringify(frames)}; }`, + silent: true, + objectId, + }); + + return objectId; } async function startDebugger(): Promise { @@ -141,13 +141,23 @@ async function startDebugger(): Promise { session.on('Debugger.paused', (event: InspectorNotification) => { isPaused = true; - handlePaused(session, stackParser, event.params as PausedExceptionEvent).then( - () => { + handlePaused(session, event.params as PausedExceptionEvent).then( + async objectId => { // After the pause work is complete, resume execution! - return isPaused ? session.post('Debugger.resume') : Promise.resolve(); + if (isPaused) { + await session.post('Debugger.resume'); + } + + if (objectId) { + // The object must be released after the debugger has resumed or we get a memory leak. + // For node v20, setImmediate is enough here but for v22 a longer delay is required + setTimeout(async () => { + await session.post('Runtime.releaseObject', { objectId }); + }, 1_000); + } }, _ => { - // ignore + // ignore any errors }, ); }); From 017e8f95be53b85792132f095ac722458d0676cc Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 10 Sep 2024 11:08:26 +0200 Subject: [PATCH 17/22] fix(nextjs): Widen removal of 404 transactions (#13628) On app router, transactions like `GET /404` get created that we don't like. --- .../nextjs-13/tests/server/404.test.ts | 23 +++++++++++++++++++ packages/nextjs/src/server/index.ts | 10 ++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/404.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/404.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/404.test.ts new file mode 100644 index 000000000000..4c09bce36b4a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/404.test.ts @@ -0,0 +1,23 @@ +import { test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create a transaction for a CJS pages router API endpoint', async ({ page }) => { + let received404Transaction = false; + waitForTransaction('nextjs-13', async transactionEvent => { + return transactionEvent.transaction === 'GET /404' || transactionEvent.transaction === 'GET /_not-found'; + }).then(() => { + received404Transaction = true; + }); + + await page.goto('/page-that-doesnt-exist'); + + await new Promise((resolve, reject) => { + setTimeout(() => { + if (received404Transaction) { + reject(new Error('received 404 transaction')); + } else { + resolve(); + } + }, 5_000); + }); +}); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 1132a6e1eed2..2348ba203021 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -219,8 +219,14 @@ export function init(options: NodeOptions): NodeClient | undefined { return null; } - // Filter out /404 transactions for pages-router which seem to be created excessively - if (event.transaction === '/404') { + // Filter out /404 transactions which seem to be created excessively + if ( + // Pages router + event.transaction === '/404' || + // App router (could be "GET /404", "POST /404", ...) + event.transaction?.match(/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/404$/) || + event.transaction === 'GET /_not-found' + ) { return null; } From cef6986afc85f3eaacc244829982c3d588131f1b Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 10 Sep 2024 11:27:28 +0200 Subject: [PATCH 18/22] feat(otel): Upgrade @opentelemetry/semantic-conventions to 1.26.0 (#13631) resolves https://github.com/getsentry/sentry-javascript/issues/13627 In 1.26.0 otel-js has updated the deprecations for the attributes based on the new changes to semantic conventions. They also changed the name of some exports, for example: `SEMATTRS_HTTP_ROUTE` -> `ATTR_HTTP_ROUTE`. Some exports names were not able to be changed because they are imported from a subpath export @opentelemetry/semantic-conventions/incubating. This subpath breaks some bundling setups, so we are unable to use it. --- packages/nextjs/package.json | 2 +- packages/nextjs/src/server/index.ts | 18 ++++++++++--- packages/node/package.json | 6 ++--- packages/node/src/integrations/tracing/koa.ts | 4 +-- packages/node/src/sdk/initOtel.ts | 9 ++++--- packages/opentelemetry/package.json | 8 +++--- packages/opentelemetry/src/propagator.ts | 7 +++--- packages/opentelemetry/src/sampler.ts | 25 +++++++++++-------- packages/opentelemetry/src/spanExporter.ts | 9 ++++--- .../src/utils/getRequestSpanData.ts | 24 +++++++++++++----- .../src/utils/isSentryRequest.ts | 8 +++--- packages/opentelemetry/src/utils/mapStatus.ts | 10 ++++++-- .../src/utils/parseSpanDescription.ts | 25 +++++++++++-------- .../opentelemetry/test/helpers/initOtel.ts | 9 ++++--- packages/opentelemetry/test/trace.test.ts | 1 + .../test/utils/getRequestSpanData.test.ts | 1 + .../test/utils/mapStatus.test.ts | 1 + .../test/utils/parseSpanDescription.test.ts | 9 ++++--- yarn.lock | 23 ++++++++++++++--- 19 files changed, 133 insertions(+), 66 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 9880e0ffb28e..8e4fd27370da 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -69,7 +69,7 @@ }, "dependencies": { "@opentelemetry/instrumentation-http": "0.53.0", - "@opentelemetry/semantic-conventions": "^1.25.1", + "@opentelemetry/semantic-conventions": "^1.27.0", "@rollup/plugin-commonjs": "26.0.1", "@sentry/core": "8.29.0", "@sentry/node": "8.29.0", diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 2348ba203021..96c97371df24 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -10,7 +10,12 @@ import { getDefaultIntegrations, init as nodeInit } from '@sentry/node'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; -import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_ROUTE, SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; +import { + ATTR_HTTP_REQUEST_METHOD, + ATTR_HTTP_ROUTE, + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_TARGET, +} from '@opentelemetry/semantic-conventions'; import type { EventProcessor } from '@sentry/types'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; @@ -150,8 +155,11 @@ export function init(options: NodeOptions): NodeClient | undefined { // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. // We need to drop these spans. if ( + // eslint-disable-next-line deprecation/deprecation typeof spanAttributes[SEMATTRS_HTTP_TARGET] === 'string' && + // eslint-disable-next-line deprecation/deprecation spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_key') && + // eslint-disable-next-line deprecation/deprecation spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_client') ) { samplingDecision.decision = false; @@ -168,8 +176,12 @@ export function init(options: NodeOptions): NodeClient | undefined { const rootSpanAttributes = spanToJSON(rootSpan).data; // Only hoist the http.route attribute if the transaction doesn't already have it - if (rootSpanAttributes?.[SEMATTRS_HTTP_METHOD] && !rootSpanAttributes?.[SEMATTRS_HTTP_ROUTE]) { - rootSpan.setAttribute(SEMATTRS_HTTP_ROUTE, spanAttributes['next.route']); + if ( + // eslint-disable-next-line deprecation/deprecation + (rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]) && + !rootSpanAttributes?.[ATTR_HTTP_ROUTE] + ) { + rootSpan.setAttribute(ATTR_HTTP_ROUTE, spanAttributes['next.route']); } } diff --git a/packages/node/package.json b/packages/node/package.json index a77c1b56b215..63c6f719a2d4 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -87,9 +87,9 @@ "@opentelemetry/instrumentation-pg": "0.44.0", "@opentelemetry/instrumentation-redis-4": "0.42.0", "@opentelemetry/instrumentation-undici": "0.6.0", - "@opentelemetry/resources": "^1.25.1", - "@opentelemetry/sdk-trace-base": "^1.25.1", - "@opentelemetry/semantic-conventions": "^1.25.1", + "@opentelemetry/resources": "^1.26.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@prisma/instrumentation": "5.19.1", "@sentry/core": "8.29.0", "@sentry/opentelemetry": "8.29.0", diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index 7db0225b6fc8..15ddc4658fa9 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -1,5 +1,5 @@ import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; -import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -29,7 +29,7 @@ export const instrumentKoa = generateInstrumentOnce( return; } const attributes = spanToJSON(span).data; - const route = attributes && attributes[SEMATTRS_HTTP_ROUTE]; + const route = attributes && attributes[ATTR_HTTP_ROUTE]; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const method: string = info?.context?.request?.method?.toUpperCase() || 'GET'; if (route) { diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 37b94ebc439f..c5ec5367f68c 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -3,9 +3,9 @@ import { DiagLogLevel, diag } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { - SEMRESATTRS_SERVICE_NAME, + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_NAMESPACE, - SEMRESATTRS_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; @@ -130,9 +130,10 @@ export function setupOtel(client: NodeClient): BasicTracerProvider { const provider = new BasicTracerProvider({ sampler: new SentrySampler(client), resource: new Resource({ - [SEMRESATTRS_SERVICE_NAME]: 'node', + [ATTR_SERVICE_NAME]: 'node', + // eslint-disable-next-line deprecation/deprecation [SEMRESATTRS_SERVICE_NAMESPACE]: 'sentry', - [SEMRESATTRS_SERVICE_VERSION]: SDK_VERSION, + [ATTR_SERVICE_VERSION]: SDK_VERSION, }), forceFlushTimeoutMillis: 500, }); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 688c5e01f2b6..cedb64423433 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -47,15 +47,15 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.25.1", "@opentelemetry/instrumentation": "^0.53.0", - "@opentelemetry/sdk-trace-base": "^1.25.1", - "@opentelemetry/semantic-conventions": "^1.25.1" + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.25.1", "@opentelemetry/core": "^1.25.1", - "@opentelemetry/sdk-trace-base": "^1.25.1", - "@opentelemetry/semantic-conventions": "^1.25.1" + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 4ed5a15532d2..387943cf9cf0 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -3,9 +3,8 @@ import { INVALID_TRACEID } from '@opentelemetry/api'; import { context } from '@opentelemetry/api'; import { propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; -import { SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; +import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; import type { continueTrace } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_URL_FULL } from '@sentry/core'; import { hasTracingEnabled } from '@sentry/core'; import { getRootSpan } from '@sentry/core'; import { spanToJSON } from '@sentry/core'; @@ -294,7 +293,9 @@ function getExistingBaggage(carrier: unknown): string | undefined { */ function getCurrentURL(span: Span): string | undefined { const spanData = spanToJSON(span).data; - const urlAttribute = spanData?.[SEMATTRS_HTTP_URL] || spanData?.[SEMANTIC_ATTRIBUTE_URL_FULL]; + // `ATTR_URL_FULL` is the new attribute, but we still support the old one, `SEMATTRS_HTTP_URL`, for now. + // eslint-disable-next-line deprecation/deprecation + const urlAttribute = spanData?.[SEMATTRS_HTTP_URL] || spanData?.[ATTR_URL_FULL]; if (urlAttribute) { return urlAttribute; } diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 03d47989ae1d..3438b4b6bbca 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -5,10 +5,8 @@ import { TraceState } from '@opentelemetry/core'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { - SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - SEMANTIC_ATTRIBUTE_URL_FULL, hasTracingEnabled, sampleSpan, } from '@sentry/core'; @@ -16,7 +14,12 @@ import type { Client, SpanAttributes } from '@sentry/types'; import { logger } from '@sentry/utils'; import { SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, SENTRY_TRACE_STATE_URL } from './constants'; -import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; +import { + ATTR_HTTP_REQUEST_METHOD, + ATTR_URL_FULL, + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_URL, +} from '@opentelemetry/semantic-conventions'; import { DEBUG_BUILD } from './debug-build'; import { getPropagationContextFromSpan } from './propagator'; import { getSamplingDecision } from './utils/getSamplingDecision'; @@ -52,13 +55,13 @@ export class SentrySampler implements Sampler { return wrapSamplingDecision({ decision: undefined, context, spanAttributes }); } + // `ATTR_HTTP_REQUEST_METHOD` is the new attribute, but we still support the old one, `SEMATTRS_HTTP_METHOD`, for now. + // eslint-disable-next-line deprecation/deprecation + const maybeSpanHttpMethod = spanAttributes[SEMATTRS_HTTP_METHOD] || spanAttributes[ATTR_HTTP_REQUEST_METHOD]; + // If we have a http.client span that has no local parent, we never want to sample it // but we want to leave downstream sampling decisions up to the server - if ( - spanKind === SpanKind.CLIENT && - (spanAttributes[SEMATTRS_HTTP_METHOD] || spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]) && - (!parentSpan || parentContext?.isRemote) - ) { + if (spanKind === SpanKind.CLIENT && maybeSpanHttpMethod && (!parentSpan || parentContext?.isRemote)) { return wrapSamplingDecision({ decision: undefined, context, spanAttributes }); } @@ -109,7 +112,7 @@ export class SentrySampler implements Sampler { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate, }; - const method = `${spanAttributes[SEMATTRS_HTTP_METHOD]}`.toUpperCase(); + const method = `${maybeSpanHttpMethod}`.toUpperCase(); if (method === 'OPTIONS' || method === 'HEAD') { DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`); @@ -198,7 +201,9 @@ function getBaseTraceState(context: Context, spanAttributes: SpanAttributes): Tr let traceState = parentContext?.traceState || new TraceState(); // We always keep the URL on the trace state, so we can access it in the propagator - const url = spanAttributes[SEMATTRS_HTTP_URL] || spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL]; + // `ATTR_URL_FULL` is the new attribute, but we still support the old one, `ATTR_HTTP_URL`, for now. + // eslint-disable-next-line deprecation/deprecation + const url = spanAttributes[SEMATTRS_HTTP_URL] || spanAttributes[ATTR_URL_FULL]; if (url && typeof url === 'string') { traceState = traceState.set(SENTRY_TRACE_STATE_URL, url); } diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 5714a3d93970..d00319ec2c98 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -1,7 +1,7 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { SEMATTRS_HTTP_STATUS_CODE } from '@opentelemetry/semantic-conventions'; +import { ATTR_HTTP_RESPONSE_STATUS_CODE, SEMATTRS_HTTP_STATUS_CODE } from '@opentelemetry/semantic-conventions'; import { captureEvent, getCapturedScopesOnSpan, @@ -358,9 +358,10 @@ function getData(span: ReadableSpan): Record { data['otel.kind'] = SpanKind[span.kind]; } - if (attributes[SEMATTRS_HTTP_STATUS_CODE]) { - const statusCode = attributes[SEMATTRS_HTTP_STATUS_CODE] as string; - data['http.response.status_code'] = statusCode; + // eslint-disable-next-line deprecation/deprecation + const maybeHttpStatusCodeAttribute = attributes[SEMATTRS_HTTP_STATUS_CODE]; + if (maybeHttpStatusCodeAttribute) { + data[ATTR_HTTP_RESPONSE_STATUS_CODE] = maybeHttpStatusCodeAttribute as string; } const requestData = getRequestSpanData(span); diff --git a/packages/opentelemetry/src/utils/getRequestSpanData.ts b/packages/opentelemetry/src/utils/getRequestSpanData.ts index 8ce4419c925d..ee723b3ca335 100644 --- a/packages/opentelemetry/src/utils/getRequestSpanData.ts +++ b/packages/opentelemetry/src/utils/getRequestSpanData.ts @@ -1,6 +1,11 @@ import type { Span } from '@opentelemetry/api'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; +import { + ATTR_HTTP_REQUEST_METHOD, + ATTR_URL_FULL, + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_URL, +} from '@opentelemetry/semantic-conventions'; import type { SanitizedRequestData } from '@sentry/types'; import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; @@ -15,9 +20,17 @@ export function getRequestSpanData(span: Span | ReadableSpan): Partial = { - url: span.attributes[SEMATTRS_HTTP_URL] as string | undefined, - 'http.method': span.attributes[SEMATTRS_HTTP_METHOD] as string | undefined, + url: maybeUrlAttribute, + // eslint-disable-next-line deprecation/deprecation + 'http.method': (span.attributes[ATTR_HTTP_REQUEST_METHOD] || span.attributes[SEMATTRS_HTTP_METHOD]) as + | string + | undefined, }; // Default to GET if URL is set but method is not @@ -26,9 +39,8 @@ export function getRequestSpanData(span: Span | ReadableSpan): Partial { [SEMATTRS_HTTP_METHOD]: 'GET', [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123', [SEMATTRS_HTTP_TARGET]: '/my-path/123', - [SEMATTRS_HTTP_ROUTE]: '/my-path/:id', + [ATTR_HTTP_ROUTE]: '/my-path/:id', }, 'test name', SpanKind.CLIENT, @@ -312,7 +313,7 @@ describe('getSanitizedUrl', () => { [SEMATTRS_HTTP_URL]: 'http://example.com/?what=true', [SEMATTRS_HTTP_METHOD]: 'GET', [SEMATTRS_HTTP_TARGET]: '/?what=true', - [SEMATTRS_HTTP_ROUTE]: '/my-route', + [ATTR_HTTP_ROUTE]: '/my-route', [SEMATTRS_HTTP_HOST]: 'example.com:80', [SEMATTRS_HTTP_STATUS_CODE]: 200, }, @@ -384,7 +385,7 @@ describe('getSanitizedUrl', () => { [SEMATTRS_HTTP_URL]: 'http://example.com/?what=true', [SEMATTRS_HTTP_METHOD]: 'GET', [SEMATTRS_HTTP_TARGET]: '/?what=true', - [SEMATTRS_HTTP_ROUTE]: '/my-route', + [ATTR_HTTP_ROUTE]: '/my-route', [SEMATTRS_HTTP_HOST]: 'example.com:80', [SEMATTRS_HTTP_STATUS_CODE]: 200, }, diff --git a/yarn.lock b/yarn.lock index 69c732db9498..22770c583774 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7330,7 +7330,7 @@ "@opentelemetry/core" "1.25.0" "@opentelemetry/semantic-conventions" "1.25.0" -"@opentelemetry/resources@1.25.1", "@opentelemetry/resources@^1.23.0", "@opentelemetry/resources@^1.25.1", "@opentelemetry/resources@^1.8.0": +"@opentelemetry/resources@1.25.1", "@opentelemetry/resources@^1.23.0", "@opentelemetry/resources@^1.8.0": version "1.25.1" resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.25.1.tgz#bb9a674af25a1a6c30840b755bc69da2796fefbb" integrity sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ== @@ -7338,6 +7338,14 @@ "@opentelemetry/core" "1.25.1" "@opentelemetry/semantic-conventions" "1.25.1" +"@opentelemetry/resources@1.26.0", "@opentelemetry/resources@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.26.0.tgz#da4c7366018bd8add1f3aa9c91c6ac59fd503cef" + integrity sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw== + dependencies: + "@opentelemetry/core" "1.26.0" + "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/resources@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-0.12.0.tgz#5eb287c3032a2bebb2bb9f69b44bd160d2a7d591" @@ -7381,7 +7389,7 @@ "@opentelemetry/resources" "1.23.0" "@opentelemetry/semantic-conventions" "1.23.0" -"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.23.0", "@opentelemetry/sdk-trace-base@^1.25.1": +"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.23.0": version "1.25.1" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz#cbc1e60af255655d2020aa14cde17b37bd13df37" integrity sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw== @@ -7390,6 +7398,15 @@ "@opentelemetry/resources" "1.25.1" "@opentelemetry/semantic-conventions" "1.25.1" +"@opentelemetry/sdk-trace-base@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz#0c913bc6d2cfafd901de330e4540952269ae579c" + integrity sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw== + dependencies: + "@opentelemetry/core" "1.26.0" + "@opentelemetry/resources" "1.26.0" + "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/semantic-conventions@1.23.0": version "1.23.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.23.0.tgz#627f2721b960fe586b7f72a07912cb7699f06eef" @@ -7400,7 +7417,7 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz#390eb4d42a29c66bdc30066af9035645e9bb7270" integrity sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ== -"@opentelemetry/semantic-conventions@1.25.1", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.23.0", "@opentelemetry/semantic-conventions@^1.25.1": +"@opentelemetry/semantic-conventions@1.25.1", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.23.0": version "1.25.1" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz#0deecb386197c5e9c2c28f2f89f51fb8ae9f145e" integrity sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ== From 1285e4bd41ca21cffcf2ca42e060170f4014ec74 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 10 Sep 2024 12:39:27 +0300 Subject: [PATCH 19/22] feat(node): Add `kafkajs` integration (#13528) --- .../node-integration-tests/package.json | 1 + .../suites/tracing/kafkajs/docker-compose.yml | 7 ++ .../suites/tracing/kafkajs/scenario.js | 63 +++++++++++++++ .../suites/tracing/kafkajs/test.ts | 55 +++++++++++++ packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/package.json | 1 + packages/node/src/index.ts | 1 + .../node/src/integrations/tracing/index.ts | 3 + .../node/src/integrations/tracing/kafka.ts | 37 +++++++++ packages/remix/src/index.server.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + yarn.lock | 77 +++++-------------- 16 files changed, 196 insertions(+), 56 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml create mode 100644 dev-packages/node-integration-tests/suites/tracing/kafkajs/scenario.js create mode 100644 dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts create mode 100644 packages/node/src/integrations/tracing/kafka.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 486b93bba24f..65c204a30dd8 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -48,6 +48,7 @@ "graphql": "^16.3.0", "http-terminator": "^3.2.0", "ioredis": "^5.4.1", + "kafkajs": "2.2.4", "mongodb": "^3.7.3", "mongodb-memory-server-global": "^7.6.3", "mongoose": "^5.13.22", diff --git a/dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml new file mode 100644 index 000000000000..f744bfe6d50c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/kafkajs/docker-compose.yml @@ -0,0 +1,7 @@ +services: + db: + image: apache/kafka:latest + restart: always + container_name: integration-tests-kafka + ports: + - '9092:9092' diff --git a/dev-packages/node-integration-tests/suites/tracing/kafkajs/scenario.js b/dev-packages/node-integration-tests/suites/tracing/kafkajs/scenario.js new file mode 100644 index 000000000000..d4541aa3a7de --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/kafkajs/scenario.js @@ -0,0 +1,63 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +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 { Kafka } = require('kafkajs'); + +async function run() { + const kafka = new Kafka({ + clientId: 'my-app', + brokers: ['localhost:9092'], + }); + + const admin = kafka.admin(); + await admin.connect(); + + const producer = kafka.producer(); + await producer.connect(); + + await admin.createTopics({ + topics: [{ topic: 'test-topic' }], + }); + + const consumer = kafka.consumer({ + groupId: 'test-group', + }); + + await consumer.connect(); + await consumer.subscribe({ topic: 'test-topic', fromBeginning: true }); + + consumer.run({ + eachMessage: async ({ message }) => { + // eslint-disable-next-line no-console + console.debug('Received message', message.value.toString()); + }, + }); + + // Wait for the consumer to be ready + await new Promise(resolve => setTimeout(resolve, 4000)); + + await producer.send({ + topic: 'test-topic', + messages: [ + { + value: 'TEST_MESSAGE', + }, + ], + }); + + // Wait for the message to be received + await new Promise(resolve => setTimeout(resolve, 5000)); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts b/dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts new file mode 100644 index 000000000000..f818af1d676a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/kafkajs/test.ts @@ -0,0 +1,55 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +// When running docker compose, we need a larger timeout, as this takes some time... +jest.setTimeout(60_000); + +describe('kafkajs', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('traces producers and consumers', done => { + createRunner(__dirname, 'scenario.js') + .withDockerCompose({ + workingDirectory: [__dirname], + readyMatches: ['9092'], + }) + .expect({ + transaction: { + transaction: 'test-topic', + contexts: { + trace: expect.objectContaining({ + op: 'message', + status: 'ok', + data: expect.objectContaining({ + 'messaging.system': 'kafka', + 'messaging.destination': 'test-topic', + 'otel.kind': 'PRODUCER', + 'sentry.op': 'message', + 'sentry.origin': 'auto.kafkajs.otel.producer', + }), + }), + }, + }, + }) + .expect({ + transaction: { + transaction: 'test-topic', + contexts: { + trace: expect.objectContaining({ + op: 'message', + status: 'ok', + data: expect.objectContaining({ + 'messaging.system': 'kafka', + 'messaging.destination': 'test-topic', + 'otel.kind': 'CONSUMER', + 'sentry.op': 'message', + 'sentry.origin': 'auto.kafkajs.otel.consumer', + }), + }), + }, + }, + }) + .start(done); + }); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 747870da3014..2645151a9ede 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -65,6 +65,7 @@ export { inboundFiltersIntegration, initOpenTelemetry, isInitialized, + kafkaIntegration, koaIntegration, lastEventId, linkedErrorsIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index f648dba045ec..19c90e3aef3f 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -89,6 +89,7 @@ export { fsIntegration, genericPoolIntegration, graphqlIntegration, + kafkaIntegration, mongoIntegration, mongooseIntegration, mysqlIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 1d8b02c33568..fcb3d1331f46 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -110,6 +110,7 @@ export { setupConnectErrorHandler, genericPoolIntegration, graphqlIntegration, + kafkaIntegration, mongoIntegration, mongooseIntegration, mysqlIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 463a0c5c1246..14aa0996cb7c 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -89,6 +89,7 @@ export { fastifyIntegration, genericPoolIntegration, graphqlIntegration, + kafkaIntegration, mongoIntegration, mongooseIntegration, mysqlIntegration, diff --git a/packages/node/package.json b/packages/node/package.json index 63c6f719a2d4..9deef7bc4fe3 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -78,6 +78,7 @@ "@opentelemetry/instrumentation-hapi": "0.41.0", "@opentelemetry/instrumentation-http": "0.53.0", "@opentelemetry/instrumentation-ioredis": "0.43.0", + "@opentelemetry/instrumentation-kafkajs": "0.3.0", "@opentelemetry/instrumentation-koa": "0.43.0", "@opentelemetry/instrumentation-mongodb": "0.47.0", "@opentelemetry/instrumentation-mongoose": "0.42.0", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 6ce3c325e3ff..d4cbcb9544a9 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -14,6 +14,7 @@ export { anrIntegration } from './integrations/anr'; export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; export { graphqlIntegration } from './integrations/tracing/graphql'; +export { kafkaIntegration } from './integrations/tracing/kafka'; export { mongoIntegration } from './integrations/tracing/mongo'; export { mongooseIntegration } from './integrations/tracing/mongoose'; export { mysqlIntegration } from './integrations/tracing/mysql'; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 46a9f79e4caa..69ffc24a8be2 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -7,6 +7,7 @@ import { fastifyIntegration, instrumentFastify } from './fastify'; import { genericPoolIntegration, instrumentGenericPool } from './genericPool'; import { graphqlIntegration, instrumentGraphql } from './graphql'; import { hapiIntegration, instrumentHapi } from './hapi'; +import { instrumentKafka, kafkaIntegration } from './kafka'; import { instrumentKoa, koaIntegration } from './koa'; import { instrumentMongo, mongoIntegration } from './mongo'; import { instrumentMongoose, mongooseIntegration } from './mongoose'; @@ -39,6 +40,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { koaIntegration(), connectIntegration(), genericPoolIntegration(), + kafkaIntegration(), ]; } @@ -53,6 +55,7 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentConnect, instrumentFastify, instrumentHapi, + instrumentKafka, instrumentKoa, instrumentNest, instrumentMongo, diff --git a/packages/node/src/integrations/tracing/kafka.ts b/packages/node/src/integrations/tracing/kafka.ts new file mode 100644 index 000000000000..7bdab00459e1 --- /dev/null +++ b/packages/node/src/integrations/tracing/kafka.ts @@ -0,0 +1,37 @@ +import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs'; + +import { defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; +import { generateInstrumentOnce } from '../../otel/instrument'; +import { addOriginToSpan } from '../../utils/addOriginToSpan'; + +const INTEGRATION_NAME = 'Kafka'; + +export const instrumentKafka = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new KafkaJsInstrumentation({ + consumerHook(span) { + addOriginToSpan(span, 'auto.kafkajs.otel.consumer'); + }, + producerHook(span) { + addOriginToSpan(span, 'auto.kafkajs.otel.producer'); + }, + }), +); + +const _kafkaIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentKafka(); + }, + }; +}) satisfies IntegrationFn; + +/** + * KafkaJs integration + * + * Capture tracing data for KafkaJs. + */ +export const kafkaIntegration = defineIntegration(_kafkaIntegration); diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 457dcb9f8685..37161b41715e 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -67,6 +67,7 @@ export { inboundFiltersIntegration, initOpenTelemetry, isInitialized, + kafkaIntegration, koaIntegration, lastEventId, linkedErrorsIntegration, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 995f58d057e3..794106eca715 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -58,6 +58,7 @@ export { inboundFiltersIntegration, initOpenTelemetry, isInitialized, + kafkaIntegration, koaIntegration, lastEventId, linkedErrorsIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index d57ec35bd7cc..e2902afb400b 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -60,6 +60,7 @@ export { inboundFiltersIntegration, initOpenTelemetry, isInitialized, + kafkaIntegration, koaIntegration, lastEventId, linkedErrorsIntegration, diff --git a/yarn.lock b/yarn.lock index 22770c583774..a9e1df0710b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7169,6 +7169,14 @@ "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" +"@opentelemetry/instrumentation-kafkajs@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.3.0.tgz#6687bce4dac8b90ef8ccbf1b662d5d1e95a34414" + integrity sha512-UnkZueYK1ise8FXQeKlpBd7YYUtC7mM8J0wzUSccEfc/G8UqHQqAzIyYCUOUPUKp8GsjLnWOOK/3hJc4owb7Jg== + dependencies: + "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/instrumentation-koa@0.43.0": version "0.43.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz#963fd192a1b5f6cbae5dabf4ec82e3105cbb23b1" @@ -9685,17 +9693,8 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": + name "@types/history-4" version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -10023,15 +10022,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -22431,6 +22422,11 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" +kafkajs@2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.4.tgz#59e6e16459d87fdf8b64be73970ed5aa42370a5b" + integrity sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA== + kareem@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93" @@ -28434,7 +28430,8 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: + name react-router-6 version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -28449,13 +28446,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -30934,16 +30924,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -31055,14 +31036,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -34031,16 +34005,7 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From a2d1b2c37f4f69a1dfc4778b2dbe52643a43e14b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Sep 2024 15:07:37 +0200 Subject: [PATCH 20/22] fix(browser): Ensure Standalone CLS span timestamps are correct (#13649) Fix a bug in the initial experimental CLS standalone span implementation. Previously we'd add the CLS start timestamp value in ms to the performance time origin timestamp which was already converted to seconds. Ensure that we first add time origin and the CLS start timestamp and then convert to seconds --------- Co-authored-by: Abhijeet Prasad --- .../web-vitals-cls-standalone-spans/test.ts | 41 +++++++++++++++++++ packages/browser-utils/src/metrics/cls.ts | 7 ++-- packages/browser-utils/src/metrics/utils.ts | 2 +- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts index cdf1e6837ef4..6defe804e665 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts @@ -453,3 +453,44 @@ sentryTest("doesn't send further CLS after the first page hide", async ({ getLoc // a timeout or something similar. await navigationTxnPromise; }); + +sentryTest('CLS span timestamps are set correctly', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.type).toBe('transaction'); + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.timestamp).toBeDefined(); + + const pageloadEndTimestamp = eventData.timestamp!; + + const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( + page, + 1, + { envelopeType: 'span' }, + properFullEnvelopeRequestParser, + ); + + await triggerAndWaitForLayoutShift(page); + + await hidePage(page); + + const spanEnvelope = (await spanEnvelopePromise)[0]; + const spanEnvelopeItem = spanEnvelope[1][0][1]; + + expect(spanEnvelopeItem.start_timestamp).toBeDefined(); + expect(spanEnvelopeItem.timestamp).toBeDefined(); + + const clsSpanStartTimestamp = spanEnvelopeItem.start_timestamp!; + const clsSpanEndTimestamp = spanEnvelopeItem.timestamp!; + + // CLS performance entries have no duration ==> start and end timestamp should be the same + expect(clsSpanStartTimestamp).toEqual(clsSpanEndTimestamp); + + // We don't really care that they are very close together but rather about the order of magnitude + // Previously, we had a bug where the timestamps would be significantly off (by multiple hours) + // so we only ensure that this bug is fixed. 60 seconds should be more than enough. + expect(clsSpanStartTimestamp - pageloadEndTimestamp).toBeLessThan(60); + expect(clsSpanStartTimestamp).toBeGreaterThan(pageloadEndTimestamp); +}); diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts index aa25a54754a1..e1d13286f5f9 100644 --- a/packages/browser-utils/src/metrics/cls.ts +++ b/packages/browser-utils/src/metrics/cls.ts @@ -84,8 +84,7 @@ export function trackClsAsStandaloneSpan(): void { function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, pageloadSpanId: string) { DEBUG_BUILD && logger.log(`Sending CLS span (${clsValue})`); - const startTime = msToSec(browserPerformanceTimeOrigin as number) + (entry?.startTime || 0); - const duration = msToSec(entry?.duration || 0); + const startTime = msToSec((browserPerformanceTimeOrigin || 0) + (entry?.startTime || 0)); const routeName = getCurrentScope().getScopeData().transactionName; const name = entry ? htmlTreeAsString(entry.sources[0]?.node) : 'Layout shift'; @@ -110,7 +109,9 @@ function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue, }); - span?.end(startTime + duration); + // LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here + // see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration + span?.end(startTime); } function supportsLayoutShift(): boolean { diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index 5f9d0de4d4ab..70327aeca838 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -86,7 +86,7 @@ export function startStandaloneWebVitalSpan(options: StandaloneWebVitalSpanOptio const user = scope.getUser(); const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined; - let profileId: string | undefined = undefined; + let profileId: string | undefined; try { // @ts-expect-error skip optional chaining to save bundle size with try catch profileId = scope.getScopeData().contexts.profile.profile_id; From ae4451df8b9de6776a285765b730738097a66b3e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 10 Sep 2024 15:11:54 +0200 Subject: [PATCH 21/22] ci: Add some additional GH project automation (#13629) Follow up to https://github.com/getsentry/sentry-javascript/pull/13608, after some tweaks and a rebase! * When a PR is opened in draft mode, move to "In Progress" on the board * When a PR is opened for review, move to "In Review" on the board * When a PR is closed but not merged, move it directly to "Done" (instead of "Ready for Release") Note that for now, this only applies to PRs that are on the board themselves, not to PRs linked to an issue. --- .github/workflows/project-automation.yml | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/project-automation.yml diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml new file mode 100644 index 000000000000..ce57e279dcf5 --- /dev/null +++ b/.github/workflows/project-automation.yml @@ -0,0 +1,98 @@ +name: "Automation: Update GH Project" +on: + pull_request: + types: + - closed + - opened + - reopened + - ready_for_review + - converted_to_draft + +jobs: + # Check if PR is in project + check_project: + name: Check if PR is in project + runs-on: ubuntu-latest + steps: + - name: Check if PR is in project + continue-on-error: true + id: check_project + uses: github/update-project-action@f980378bc179626af5b4e20ec05ec39c7f7a6f6d + with: + github_token: ${{ secrets.GH_PROJECT_AUTOMATION }} + organization: getsentry + project_number: 31 + content_id: ${{ github.event.pull_request.node_id }} + field: Status + operation: read + + - name: If project field is read, set is_in_project to 1 + if: steps.check_project.outputs.field_read_value + id: is_in_project + run: echo "is_in_project=1" >> "$GITHUB_OUTPUT" + + outputs: + is_in_project: ${{ steps.is_in_project.outputs.is_in_project || '0' }} + + # When a PR is a draft, it should go into "In Progress" + mark_as_in_progress: + name: "Mark as In Progress" + needs: check_project + if: | + needs.check_project.outputs.is_in_project == '1' + && (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'converted_to_draft') + && github.event.pull_request.draft == true + runs-on: ubuntu-latest + steps: + - name: Update status to in_progress + uses: github/update-project-action@f980378bc179626af5b4e20ec05ec39c7f7a6f6d + with: + github_token: ${{ secrets.GH_PROJECT_AUTOMATION }} + organization: getsentry + project_number: 31 + content_id: ${{ github.event.pull_request.node_id }} + field: Status + value: "🏗 In Progress" + + # When a PR is not a draft, it should go into "In Review" + mark_as_in_review: + name: "Mark as In Review" + needs: check_project + if: | + needs.check_project.outputs.is_in_project == '1' + && (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'ready_for_review') + && github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Update status to in_review + id: update_status + uses: github/update-project-action@f980378bc179626af5b4e20ec05ec39c7f7a6f6d + with: + github_token: ${{ secrets.GH_PROJECT_AUTOMATION }} + organization: getsentry + project_number: 31 + content_id: ${{ github.event.pull_request.node_id }} + field: Status + value: "👀 In Review" + + # By default, closed PRs go into "Ready for Release" + # But if they are closed without merging, they should go into "Done" + mark_as_done: + name: "Mark as Done" + needs: check_project + if: | + needs.check_project.outputs.is_in_project == '1' + && github.event.action == 'closed' && github.event.pull_request.merged == false + runs-on: ubuntu-latest + steps: + - name: Update status to done + id: update_status + uses: github/update-project-action@f980378bc179626af5b4e20ec05ec39c7f7a6f6d + with: + github_token: ${{ secrets.GH_PROJECT_AUTOMATION }} + organization: getsentry + project_number: 31 + content_id: ${{ github.event.pull_request.node_id }} + field: Status + value: "✅ Done" + From aba68f7e0baf65e3326b6af5afd1e1d711ca96c9 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 10 Sep 2024 13:58:35 +0200 Subject: [PATCH 22/22] meta: Update CHANGELOG for 8.30.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da1f91d499bb..7bf504ce80b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,38 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.30.0 + +### Important Changes + +- _feat(node): Add `kafkajs` integration (#13528)_ + +This release adds a new integration that instruments `kafkajs` library with spans and traces. This integration is +automatically enabled by default, but can be included with the `Sentry.kafkaIntegration()` import. + +```js +Sentry.init({ + integrations: [Sentry.kafkaIntegration()], +}); +``` + +### Other Changes + +- feat(core): Allow adding measurements without global client (#13612) +- feat(deps): Bump @opentelemetry/instrumentation-undici from 0.5.0 to 0.6.0 (#13622) +- feat(deps): Bump @sentry/cli from 2.33.0 to 2.35.0 (#13624) +- feat(node): Use `@opentelemetry/instrumentation-undici` for fetch tracing (#13485) +- feat(nuxt): Add server config to root folder (#13583) +- feat(otel): Upgrade @opentelemetry/semantic-conventions to 1.26.0 (#13631) +- fix(browser): check supportedEntryTypes before caling the function (#13541) +- fix(browser): Ensure Standalone CLS span timestamps are correct (#13649) +- fix(nextjs): Widen removal of 404 transactions (#13628) +- fix(node): Remove ambiguity and race conditions when matching local variables to exceptions (#13501) +- fix(node): Update OpenTelemetry instrumentation package for solidstart and opentelemetry (#13640) +- fix(node): Update OpenTelemetry instrumentation package for solidstart and opentelemetry (#13642) +- fix(vue): Ensure Vue `trackComponents` list matches components with or without `<>` (#13543) +- ref(profiling): Conditionally shim cjs globals (#13267) + Work in this release was contributed by @Zen-cronic and @odanado. Thank you for your contributions! ## 8.29.0