From 68bf926aabf1719b9a98688adf233e64f9af558d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 18 Oct 2023 11:48:33 +0200 Subject: [PATCH] feat(opentelemetry): Add new `@sentry/opentelemetry` package Apply suggestions from code review Co-authored-by: Abhijeet Prasad PR feedback PR feedback: Expose proper `setupGlobalHub` method pr feedback & new release docs --- .craft.yml | 4 + docs/new-sdk-release-checklist.md | 6 +- package.json | 1 + .../e2e-tests/verdaccio-config/config.yaml | 6 + packages/node-experimental/README.md | 2 +- packages/node-experimental/package.json | 36 +- packages/node-experimental/src/constants.ts | 21 - packages/node-experimental/src/index.ts | 5 +- .../src/integrations/http.ts | 13 +- .../src/integrations/node-fetch.ts | 12 +- .../src/opentelemetry/contextManager.ts | 37 -- .../src/opentelemetry/spanProcessor.ts | 99 ---- packages/node-experimental/src/sdk/client.ts | 74 +-- packages/node-experimental/src/sdk/init.ts | 12 +- .../node-experimental/src/sdk/initOtel.ts | 22 +- .../src/sdk/spanProcessor.ts | 39 ++ packages/node-experimental/src/types.ts | 18 +- .../src/utils/addOriginToSpan.ts | 5 +- .../test/integration/breadcrumbs.test.ts | 18 +- .../test/integration/scope.test.ts | 29 +- .../test/integration/transactions.test.ts | 11 +- packages/opentelemetry-node/package.json | 8 +- .../test/spanprocessor.test.ts | 4 +- packages/opentelemetry/.eslintrc.js | 9 + packages/opentelemetry/LICENSE | 14 + packages/opentelemetry/README.md | 98 ++++ packages/opentelemetry/jest.config.js | 1 + packages/opentelemetry/package.json | 73 +++ packages/opentelemetry/rollup.npm.config.js | 3 + .../src/asyncContextStrategy.ts} | 7 +- packages/opentelemetry/src/constants.ts | 10 + packages/opentelemetry/src/contextManager.ts | 57 ++ packages/opentelemetry/src/custom/client.ts | 81 +++ .../sdk => opentelemetry/src/custom}/hub.ts | 28 +- .../src/custom}/hubextensions.ts | 2 +- .../sdk => opentelemetry/src/custom}/scope.ts | 39 +- .../src/custom}/transaction.ts | 4 +- packages/opentelemetry/src/index.ts | 47 ++ .../src}/propagator.ts | 15 +- .../src}/sampler.ts | 19 +- .../opentelemetry/src/semanticAttributes.ts | 16 + .../src}/setupEventContextTrace.ts | 4 +- .../src}/spanExporter.ts | 74 +-- packages/opentelemetry/src/spanProcessor.ts | 86 +++ .../src/sdk => opentelemetry/src}/trace.ts | 65 +-- packages/opentelemetry/src/types.ts | 29 + .../src/utils/addOriginToSpan.ts | 9 + .../utils/captureExceptionForTimedEvent.ts | 55 ++ .../opentelemetry/src/utils/contextData.ts | 36 ++ .../src/utils/convertOtelTimeToSeconds.ts | 0 .../src/utils/getActiveSpan.ts | 2 +- .../src/utils/getRequestSpanData.ts | 0 .../src/utils/getSpanKind.ts | 4 +- .../src/utils/groupSpansWithParents.ts | 2 +- .../src/utils/isSentryRequest.ts | 26 + packages/opentelemetry/src/utils/mapStatus.ts | 74 +++ .../src/utils/parseSpanDescription.ts | 166 ++++++ .../src/utils}/spanData.ts | 0 .../src/utils/spanTypes.ts | 32 +- .../test/asyncContextStrategy.test.ts} | 21 +- .../opentelemetry/test/custom/client.test.ts | 19 + .../test/custom}/hub.test.ts | 22 +- .../test/custom}/hubextensions.test.ts | 11 +- .../test/custom}/scope.test.ts | 90 ++- .../test/custom}/transaction.test.ts | 31 +- .../opentelemetry/test/helpers/TestClient.ts | 49 ++ .../opentelemetry/test/helpers/createSpan.ts | 30 + .../opentelemetry/test/helpers/initOtel.ts | 72 +++ .../opentelemetry/test/helpers/mockSdkInit.ts | 68 +++ .../test/integration/breadcrumbs.test.ts | 361 ++++++++++++ .../test/integration/otelTimedEvents.test.ts | 14 +- .../test/integration/scope.test.ts | 238 ++++++++ .../test/integration/transactions.test.ts | 536 ++++++++++++++++++ .../test}/propagator.test.ts | 40 +- .../sdk => opentelemetry/test}/trace.test.ts | 338 ++++++----- .../captureExceptionForTimedEvent.test.ts | 147 +++++ .../utils/convertOtelTimeToSeconds.test.ts | 0 .../test/utils/getActiveSpan.test.ts | 11 +- .../test/utils/getRequestSpanData.test.ts | 0 .../test/utils/getSpanKind.test.ts | 11 + .../test/utils/groupSpansWithParents.test.ts | 0 .../test/utils/mapStatus.test.ts | 83 +++ .../test/utils/parseSpanDescription.test.ts | 340 +++++++++++ .../test/utils/setupEventContextTrace.test.ts | 25 +- .../test/utils/spanTypes.test.ts | 27 +- packages/opentelemetry/tsconfig.json | 9 + packages/opentelemetry/tsconfig.test.json | 12 + packages/opentelemetry/tsconfig.types.json | 10 + scripts/node-unit-tests.ts | 3 + yarn.lock | 293 +++++----- 90 files changed, 3618 insertions(+), 962 deletions(-) delete mode 100644 packages/node-experimental/src/constants.ts delete mode 100644 packages/node-experimental/src/opentelemetry/contextManager.ts delete mode 100644 packages/node-experimental/src/opentelemetry/spanProcessor.ts create mode 100644 packages/node-experimental/src/sdk/spanProcessor.ts create mode 100644 packages/opentelemetry/.eslintrc.js create mode 100644 packages/opentelemetry/LICENSE create mode 100644 packages/opentelemetry/README.md create mode 100644 packages/opentelemetry/jest.config.js create mode 100644 packages/opentelemetry/package.json create mode 100644 packages/opentelemetry/rollup.npm.config.js rename packages/{node-experimental/src/sdk/otelAsyncContextStrategy.ts => opentelemetry/src/asyncContextStrategy.ts} (82%) create mode 100644 packages/opentelemetry/src/constants.ts create mode 100644 packages/opentelemetry/src/contextManager.ts create mode 100644 packages/opentelemetry/src/custom/client.ts rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/hub.ts (83%) rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/hubextensions.ts (90%) rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/scope.ts (77%) rename packages/{node-experimental/src/sdk => opentelemetry/src/custom}/transaction.ts (90%) create mode 100644 packages/opentelemetry/src/index.ts rename packages/{node-experimental/src/opentelemetry => opentelemetry/src}/propagator.ts (90%) rename packages/{node-experimental/src/opentelemetry => opentelemetry/src}/sampler.ts (90%) create mode 100644 packages/opentelemetry/src/semanticAttributes.ts rename packages/{node-experimental/src/utils => opentelemetry/src}/setupEventContextTrace.ts (86%) rename packages/{node-experimental/src/opentelemetry => opentelemetry/src}/spanExporter.ts (79%) create mode 100644 packages/opentelemetry/src/spanProcessor.ts rename packages/{node-experimental/src/sdk => opentelemetry/src}/trace.ts (50%) create mode 100644 packages/opentelemetry/src/types.ts create mode 100644 packages/opentelemetry/src/utils/addOriginToSpan.ts create mode 100644 packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts create mode 100644 packages/opentelemetry/src/utils/contextData.ts rename packages/{node-experimental => opentelemetry}/src/utils/convertOtelTimeToSeconds.ts (100%) rename packages/{node-experimental => opentelemetry}/src/utils/getActiveSpan.ts (89%) rename packages/{node-experimental => opentelemetry}/src/utils/getRequestSpanData.ts (100%) rename packages/{node-experimental => opentelemetry}/src/utils/getSpanKind.ts (80%) rename packages/{node-experimental => opentelemetry}/src/utils/groupSpansWithParents.ts (97%) create mode 100644 packages/opentelemetry/src/utils/isSentryRequest.ts create mode 100644 packages/opentelemetry/src/utils/mapStatus.ts create mode 100644 packages/opentelemetry/src/utils/parseSpanDescription.ts rename packages/{node-experimental/src/opentelemetry => opentelemetry/src/utils}/spanData.ts (100%) rename packages/{node-experimental => opentelemetry}/src/utils/spanTypes.ts (68%) rename packages/{node-experimental/test/sdk/otelAsyncContextStrategy.test.ts => opentelemetry/test/asyncContextStrategy.test.ts} (84%) create mode 100644 packages/opentelemetry/test/custom/client.test.ts rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/hub.test.ts (52%) rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/hubextensions.test.ts (52%) rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/scope.test.ts (81%) rename packages/{node-experimental/test/sdk => opentelemetry/test/custom}/transaction.test.ts (77%) create mode 100644 packages/opentelemetry/test/helpers/TestClient.ts create mode 100644 packages/opentelemetry/test/helpers/createSpan.ts create mode 100644 packages/opentelemetry/test/helpers/initOtel.ts create mode 100644 packages/opentelemetry/test/helpers/mockSdkInit.ts create mode 100644 packages/opentelemetry/test/integration/breadcrumbs.test.ts rename packages/{node-experimental => opentelemetry}/test/integration/otelTimedEvents.test.ts (76%) create mode 100644 packages/opentelemetry/test/integration/scope.test.ts create mode 100644 packages/opentelemetry/test/integration/transactions.test.ts rename packages/{node-experimental/test/opentelemetry => opentelemetry/test}/propagator.test.ts (90%) rename packages/{node-experimental/test/sdk => opentelemetry/test}/trace.test.ts (52%) create mode 100644 packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts rename packages/{node-experimental => opentelemetry}/test/utils/convertOtelTimeToSeconds.test.ts (100%) rename packages/{node-experimental => opentelemetry}/test/utils/getActiveSpan.test.ts (88%) rename packages/{node-experimental => opentelemetry}/test/utils/getRequestSpanData.test.ts (100%) create mode 100644 packages/opentelemetry/test/utils/getSpanKind.test.ts rename packages/{node-experimental => opentelemetry}/test/utils/groupSpansWithParents.test.ts (100%) create mode 100644 packages/opentelemetry/test/utils/mapStatus.test.ts create mode 100644 packages/opentelemetry/test/utils/parseSpanDescription.test.ts rename packages/{node-experimental => opentelemetry}/test/utils/setupEventContextTrace.test.ts (77%) rename packages/{node-experimental => opentelemetry}/test/utils/spanTypes.test.ts (72%) create mode 100644 packages/opentelemetry/tsconfig.json create mode 100644 packages/opentelemetry/tsconfig.test.json create mode 100644 packages/opentelemetry/tsconfig.types.json diff --git a/.craft.yml b/.craft.yml index 039528edc0a9..521e1e4f8ba3 100644 --- a/.craft.yml +++ b/.craft.yml @@ -24,6 +24,10 @@ targets: - name: npm id: '@sentry/replay' includeNames: /^sentry-replay-\d.*\.tgz$/ + ## 1.6. OpenTelemetry package + - name: npm + id: '@sentry/opentelemetry' + includeNames: /^sentry-opentelemetry-\d.*\.tgz$/ ## 2. Browser & Node SDKs - name: npm diff --git a/docs/new-sdk-release-checklist.md b/docs/new-sdk-release-checklist.md index 1b40763cdd6c..c02f0d01c1ef 100644 --- a/docs/new-sdk-release-checklist.md +++ b/docs/new-sdk-release-checklist.md @@ -47,6 +47,8 @@ This page serves as a checklist of what to do when releasing a new SDK for the f - [ ] Make sure it is added to `bundlePlugins.ts:makeTSPlugin` as `paths`, otherwise it will not be ES5 transpiled correctly for CDN builds. +- [ ] Make sure it is added to the [Verdaccio config](https://github.com/getsentry/sentry-javascript/blob/develop/packages/e2e-tests/verdaccio-config/config.yaml) for the E2E tests + ## Cutting the Release When you’re ready to make the first release, there are a couple of steps that need to be performed in the **correct order**. Note that you can prepare the PRs at any time but the **merging oder** is important: @@ -56,7 +58,7 @@ When you’re ready to make the first release, there are a couple of steps that ### Before the Release: - [ ] 1) If not yet done, be sure to remove the `private: true` property from your SDK’s `package.json`. Additionally, ensure that `"publishConfig": {"access": "public"}` is set. -- [ ] 2) Make sure that the new SDK is **not added** in`[craft.yml](https://github.com/getsentry/sentry-javascript/blob/master/.craft.yml)` as a target for the **Sentry release registry**\ +- [ ] 2) Make sure that the new SDK is **not added** in`[craft.yml](https://github.com/getsentry/sentry-javascript/blob/develop/.craft.yml)` as a target for the **Sentry release registry**\ *Once this is added, craft will try to publish an entry in the next release which does not work and caused failed release runs in the past* - [ ] 3) Add an `npm` target in `craft.yml` for the new package. Make sure to insert it in the right place, after all the Sentry dependencies of your package but before packages that depend on your new package (if applicable). ```yml @@ -74,7 +76,7 @@ When you’re ready to make the first release, there are a couple of steps that You have to fork this repo and PR the files from your fork to the main repo \ [Example PR](https://github.com/getsentry/sentry-release-registry/pull/80) from the Svelte SDK -- [ ] 2) Add an entry to `[craft.yml](https://github.com/getsentry/sentry-javascript/blob/master/.craft.yml)` to add releases of your SDK to the Sentry release registry \ +- [ ] 2) Add an entry to [craft.yml](https://github.com/getsentry/sentry-javascript/blob/develop/.craft.yml) to add releases of your SDK to the Sentry release registry \ [Example PR](https://github.com/getsentry/sentry-javascript/pull/5547) from the Svelte SDK \ *Subsequent releases will now be added automatically to the registry* diff --git a/package.json b/package.json index 364c97a53a75..c1ffba595329 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "packages/node-integration-tests", "packages/node-experimental", "packages/opentelemetry-node", + "packages/opentelemetry", "packages/react", "packages/remix", "packages/replay", diff --git a/packages/e2e-tests/verdaccio-config/config.yaml b/packages/e2e-tests/verdaccio-config/config.yaml index 938b877a50e5..0f1fdee05669 100644 --- a/packages/e2e-tests/verdaccio-config/config.yaml +++ b/packages/e2e-tests/verdaccio-config/config.yaml @@ -122,6 +122,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/opentelemetry': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/react': access: $all publish: $all diff --git a/packages/node-experimental/README.md b/packages/node-experimental/README.md index 5bebaa336e17..e23ee4f1817c 100644 --- a/packages/node-experimental/README.md +++ b/packages/node-experimental/README.md @@ -82,7 +82,7 @@ const span = Sentry.startSpan({ description: 'non-active span' }); doSomethingSlow(); -span?.finish(); +span.finish(); ``` Finally you can also get the currently active span, if you need to do more with it: diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index a7f41fea10c8..dd3a5bd3afee 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -24,26 +24,26 @@ }, "dependencies": { "@opentelemetry/api": "~1.6.0", - "@opentelemetry/context-async-hooks": "~1.17.0", - "@opentelemetry/core": "~1.17.0", - "@opentelemetry/instrumentation": "~0.43.0", - "@opentelemetry/instrumentation-express": "~0.33.1", - "@opentelemetry/instrumentation-fastify": "~0.32.3", - "@opentelemetry/instrumentation-graphql": "~0.35.1", - "@opentelemetry/instrumentation-http": "~0.43.0", - "@opentelemetry/instrumentation-mongodb": "~0.37.0", - "@opentelemetry/instrumentation-mongoose": "~0.33.1", - "@opentelemetry/instrumentation-mysql": "~0.34.1", - "@opentelemetry/instrumentation-mysql2": "~0.34.1", - "@opentelemetry/instrumentation-nestjs-core": "~0.33.1", - "@opentelemetry/instrumentation-pg": "~0.36.1", - "@opentelemetry/resources": "~1.17.0", - "@opentelemetry/sdk-trace-base": "~1.17.0", - "@opentelemetry/semantic-conventions": "~1.17.0", - "@prisma/instrumentation": "~5.3.1", + "@opentelemetry/core": "~1.17.1", + "@opentelemetry/context-async-hooks": "~1.17.1", + "@opentelemetry/instrumentation": "0.44.0", + "@opentelemetry/instrumentation-express": "0.33.2", + "@opentelemetry/instrumentation-fastify": "0.32.3", + "@opentelemetry/instrumentation-graphql": "0.35.2", + "@opentelemetry/instrumentation-http": "0.44.0", + "@opentelemetry/instrumentation-mongodb": "0.37.1", + "@opentelemetry/instrumentation-mongoose": "0.33.2", + "@opentelemetry/instrumentation-mysql": "0.34.2", + "@opentelemetry/instrumentation-mysql2": "0.34.2", + "@opentelemetry/instrumentation-nestjs-core": "0.33.2", + "@opentelemetry/instrumentation-pg": "0.36.2", + "@opentelemetry/resources": "~1.17.1", + "@opentelemetry/sdk-trace-base": "~1.17.1", + "@opentelemetry/semantic-conventions": "~1.17.1", + "@prisma/instrumentation": "5.4.2", "@sentry/core": "7.74.1", "@sentry/node": "7.74.1", - "@sentry/opentelemetry-node": "7.74.1", + "@sentry/opentelemetry": "7.74.1", "@sentry/types": "7.74.1", "@sentry/utils": "7.74.1", "opentelemetry-instrumentation-fetch-node": "1.1.0" diff --git a/packages/node-experimental/src/constants.ts b/packages/node-experimental/src/constants.ts deleted file mode 100644 index 8d06aa411c1c..000000000000 --- a/packages/node-experimental/src/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createContextKey } from '@opentelemetry/api'; - -export const OTEL_CONTEXT_HUB_KEY = createContextKey('sentry_hub'); - -export const OTEL_ATTR_ORIGIN = 'sentry.origin'; -export const OTEL_ATTR_OP = 'sentry.op'; -export const OTEL_ATTR_SOURCE = 'sentry.source'; - -export const OTEL_ATTR_PARENT_SAMPLED = 'sentry.parentSampled'; - -export const OTEL_ATTR_BREADCRUMB_TYPE = 'sentry.breadcrumb.type'; -export const OTEL_ATTR_BREADCRUMB_LEVEL = 'sentry.breadcrumb.level'; -export const OTEL_ATTR_BREADCRUMB_EVENT_ID = 'sentry.breadcrumb.event_id'; -export const OTEL_ATTR_BREADCRUMB_CATEGORY = 'sentry.breadcrumb.category'; -export const OTEL_ATTR_BREADCRUMB_DATA = 'sentry.breadcrumb.data'; -export const OTEL_ATTR_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; - -export const SENTRY_TRACE_HEADER = 'sentry-trace'; -export const SENTRY_BAGGAGE_HEADER = 'baggage'; - -export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY'); diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index db1abe96495a..e19b1231712f 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -11,11 +11,10 @@ export { init } from './sdk/init'; export { INTEGRATIONS as Integrations }; export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations'; export * as Handlers from './sdk/handlers'; -export * from './sdk/trace'; -export { getActiveSpan } from './utils/getActiveSpan'; -export { getCurrentHub, getHubFromCarrier } from './sdk/hub'; export type { Span } from './types'; +export { startSpan, startInactiveSpan, getCurrentHub, getActiveSpan } from '@sentry/opentelemetry'; + export { makeNodeTransport, defaultStackParser, diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 5b939e2ead20..69712e503761 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -3,17 +3,14 @@ import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; +import { _INTERNAL, getCurrentHub, getSpanKind, setSpanMetadata } from '@sentry/opentelemetry'; import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { stringMatchesSomePattern } from '@sentry/utils'; import type { ClientRequest, IncomingMessage, ServerResponse } from 'http'; -import { OTEL_ATTR_ORIGIN } from '../constants'; -import { setSpanMetadata } from '../opentelemetry/spanData'; -import type { NodeExperimentalClient } from '../sdk/client'; -import { getCurrentHub } from '../sdk/hub'; -import { getRequestSpanData } from '../utils/getRequestSpanData'; +import type { NodeExperimentalClient } from '../types'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { getRequestUrl } from '../utils/getRequestUrl'; -import { getSpanKind } from '../utils/getSpanKind'; interface HttpOptions { /** @@ -148,7 +145,7 @@ export class Http implements Integration { /** Update the span with data we need. */ private _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void { - span.setAttribute(OTEL_ATTR_ORIGIN, 'auto.http.otel.http'); + addOriginToSpan(span, 'auto.http.otel.http'); if (getSpanKind(span) === SpanKind.SERVER) { setSpanMetadata(span, { request }); @@ -161,7 +158,7 @@ export class Http implements Integration { return; } - const data = getRequestSpanData(span); + const data = _INTERNAL.getRequestSpanData(span); getCurrentHub().addBreadcrumb( { category: 'http', diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node-experimental/src/integrations/node-fetch.ts index 9afd70be62e7..281c6f6d6784 100644 --- a/packages/node-experimental/src/integrations/node-fetch.ts +++ b/packages/node-experimental/src/integrations/node-fetch.ts @@ -2,14 +2,12 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { Instrumentation } from '@opentelemetry/instrumentation'; import { hasTracingEnabled } from '@sentry/core'; +import { _INTERNAL, getCurrentHub, getSpanKind } from '@sentry/opentelemetry'; import type { Integration } from '@sentry/types'; import { FetchInstrumentation } from 'opentelemetry-instrumentation-fetch-node'; -import { OTEL_ATTR_ORIGIN } from '../constants'; -import type { NodeExperimentalClient } from '../sdk/client'; -import { getCurrentHub } from '../sdk/hub'; -import { getRequestSpanData } from '../utils/getRequestSpanData'; -import { getSpanKind } from '../utils/getSpanKind'; +import type { NodeExperimentalClient } from '../types'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; interface NodeFetchOptions { @@ -101,7 +99,7 @@ export class NodeFetch extends NodePerformanceIntegration impl /** Update the span with data we need. */ private _updateSpan(span: Span): void { - span.setAttribute(OTEL_ATTR_ORIGIN, 'auto.http.otel.node_fetch'); + addOriginToSpan(span, 'auto.http.otel.node_fetch'); } /** Add a breadcrumb for outgoing requests. */ @@ -110,7 +108,7 @@ export class NodeFetch extends NodePerformanceIntegration impl return; } - const data = getRequestSpanData(span); + const data = _INTERNAL.getRequestSpanData(span); getCurrentHub().addBreadcrumb({ category: 'http', data: { diff --git a/packages/node-experimental/src/opentelemetry/contextManager.ts b/packages/node-experimental/src/opentelemetry/contextManager.ts deleted file mode 100644 index 438a2c49fac7..000000000000 --- a/packages/node-experimental/src/opentelemetry/contextManager.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Context } from '@opentelemetry/api'; -import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; -import type { Carrier, Hub } from '@sentry/core'; - -import { OTEL_CONTEXT_HUB_KEY } from '../constants'; -import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './../sdk/hub'; - -function createNewHub(parent: Hub | undefined): Hub { - const carrier: Carrier = {}; - ensureHubOnCarrier(carrier, parent); - return getHubFromCarrier(carrier); -} - -/** - * This is a custom ContextManager for OpenTelemetry, which extends the default AsyncLocalStorageContextManager. - * It ensures that we create a new hub per context, so that the OTEL Context & the Sentry Hub are always in sync. - * - * Note that we currently only support AsyncHooks with this, - * but since this should work for Node 14+ anyhow that should be good enough. - */ -export class SentryContextManager extends AsyncLocalStorageContextManager { - /** - * Overwrite with() of the original AsyncLocalStorageContextManager - * to ensure we also create a new hub per context. - */ - public with ReturnType>( - context: Context, - fn: F, - thisArg?: ThisParameterType, - ...args: A - ): ReturnType { - const existingHub = getCurrentHub(); - const newHub = createNewHub(existingHub); - - return super.with(context.setValue(OTEL_CONTEXT_HUB_KEY, newHub), fn, thisArg, ...args); - } -} diff --git a/packages/node-experimental/src/opentelemetry/spanProcessor.ts b/packages/node-experimental/src/opentelemetry/spanProcessor.ts deleted file mode 100644 index c7e07d11aa8e..000000000000 --- a/packages/node-experimental/src/opentelemetry/spanProcessor.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { Context } from '@opentelemetry/api'; -import { ROOT_CONTEXT, SpanKind, trace } from '@opentelemetry/api'; -import type { Span, SpanProcessor as SpanProcessorInterface } from '@opentelemetry/sdk-trace-base'; -import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { maybeCaptureExceptionForTimedEvent } from '@sentry/opentelemetry-node'; -import type { Hub } from '@sentry/types'; -import { logger } from '@sentry/utils'; - -import { OTEL_CONTEXT_HUB_KEY } from '../constants'; -import { Http } from '../integrations'; -import { NodeFetch } from '../integrations/node-fetch'; -import type { NodeExperimentalClient } from '../sdk/client'; -import { getCurrentHub } from '../sdk/hub'; -import { getSpanHub, setSpanHub, setSpanParent, setSpanScope } from './spanData'; -import { SentrySpanExporter } from './spanExporter'; - -/** - * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via - * the Sentry SDK. - */ -export class SentrySpanProcessor extends BatchSpanProcessor implements SpanProcessorInterface { - public constructor() { - super(new SentrySpanExporter()); - } - - /** - * @inheritDoc - */ - public onStart(span: Span, parentContext: Context): void { - // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK - const parentSpan = trace.getSpan(parentContext); - const hub = parentContext.getValue(OTEL_CONTEXT_HUB_KEY) as Hub | undefined; - - // We need access to the parent span in order to be able to move up the span tree for breadcrumbs - if (parentSpan) { - setSpanParent(span, parentSpan); - } - - // The root context does not have a hub stored, so we check for this specifically - // We do this instead of just falling back to `getCurrentHub` to avoid attaching the wrong hub - let actualHub = hub; - if (parentContext === ROOT_CONTEXT) { - actualHub = getCurrentHub(); - } - - // We need the scope at time of span creation in order to apply it to the event when the span is finished - if (actualHub) { - setSpanScope(span, actualHub.getScope()); - setSpanHub(span, actualHub); - } - - __DEBUG_BUILD__ && logger.log(`[Tracing] Starting span "${span.name}" (${span.spanContext().spanId})`); - - return super.onStart(span, parentContext); - } - - /** @inheritDoc */ - public onEnd(span: Span): void { - __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing span "${span.name}" (${span.spanContext().spanId})`); - - if (!shouldCaptureSentrySpan(span)) { - // Prevent this being called to super.onEnd(), which would pass this to the span exporter - return; - } - - // Capture exceptions as events - const hub = getSpanHub(span) || getCurrentHub(); - span.events.forEach(event => { - maybeCaptureExceptionForTimedEvent(hub, event, span); - }); - - return super.onEnd(span); - } -} - -function shouldCaptureSentrySpan(span: Span): boolean { - const client = getCurrentHub().getClient(); - const httpIntegration = client ? client.getIntegration(Http) : undefined; - const fetchIntegration = client ? client.getIntegration(NodeFetch) : undefined; - - // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation - // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, - // So we can generate a breadcrumb for it but no span will be sent - if ( - (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && - span.attributes[SemanticAttributes.HTTP_URL] && - span.attributes[SemanticAttributes.HTTP_METHOD] - ) { - const shouldCreateSpansForRequests = - span.attributes['http.client'] === 'fetch' - ? fetchIntegration?.shouldCreateSpansForRequests - : httpIntegration?.shouldCreateSpansForRequests; - - return shouldCreateSpansForRequests !== false; - } - - return true; -} diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index a3145475e307..809d1fa49035 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -1,23 +1,7 @@ -import type { Tracer } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; -import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import type { EventHint, Scope } from '@sentry/node'; import { NodeClient, SDK_VERSION } from '@sentry/node'; -import type { Event } from '@sentry/types'; - -import type { - NodeExperimentalClient as NodeExperimentalClientInterface, - NodeExperimentalClientOptions, -} from '../types'; -import { NodeExperimentalScope } from './scope'; - -/** - * A client built on top of the NodeClient, which provides some otel-specific things on top. - */ -export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface { - public traceProvider: BasicTracerProvider | undefined; - private _tracer: Tracer | undefined; +import { wrapClientClass } from '@sentry/opentelemetry'; +class NodeExperimentalBaseClient extends NodeClient { public constructor(options: ConstructorParameters[0]) { options._metadata = options._metadata || {}; options._metadata.sdk = options._metadata.sdk || { @@ -33,56 +17,6 @@ export class NodeExperimentalClient extends NodeClient implements NodeExperiment super(options); } - - /** Get the OTEL tracer. */ - public get tracer(): Tracer { - if (this._tracer) { - return this._tracer; - } - - const name = '@sentry/node-experimental'; - const version = SDK_VERSION; - const tracer = trace.getTracer(name, version); - this._tracer = tracer; - - return tracer; - } - - /** - * Get the options for the node preview client. - */ - public getOptions(): NodeExperimentalClientOptions { - // Just a type-cast, basically - return super.getOptions(); - } - - /** - * @inheritDoc - */ - public async flush(timeout?: number): Promise { - const provider = this.traceProvider; - const spanProcessor = provider?.activeSpanProcessor; - - if (spanProcessor) { - await spanProcessor.forceFlush(); - } - - return super.flush(timeout); - } - - /** - * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. - * This uses `Scope.clone()`, which we need to replace with `NodeExperimentalScope.clone()` for this client. - */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { - let actualScope = scope; - - // Remove `captureContext` hint and instead clone already here - if (hint && hint.captureContext) { - actualScope = NodeExperimentalScope.clone(scope); - delete hint.captureContext; - } - - return super._prepareEvent(event, hint, actualScope); - } } + +export const NodeExperimentalClient = wrapClientClass(NodeExperimentalBaseClient); diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index c33a90f037d7..be4843a5d2f7 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,5 +1,7 @@ import { hasTracingEnabled } from '@sentry/core'; +import type { NodeClient } from '@sentry/node'; import { defaultIntegrations as defaultNodeIntegrations, init as initNode } from '@sentry/node'; +import { setOpenTelemetryContextAsyncContextStrategy, setupGlobalHub } from '@sentry/opentelemetry'; import type { Integration } from '@sentry/types'; import { parseSemver } from '@sentry/utils'; @@ -8,9 +10,7 @@ import { Http } from '../integrations/http'; import { NodeFetch } from '../integrations/node-fetch'; import type { NodeExperimentalOptions } from '../types'; import { NodeExperimentalClient } from './client'; -import { getCurrentHub } from './hub'; import { initOtel } from './initOtel'; -import { setOtelContextAsyncContextStrategy } from './otelAsyncContextStrategy'; const NODE_VERSION: ReturnType = parseSemver(process.versions.node); const ignoredDefaultIntegrations = ['Http', 'Undici']; @@ -29,9 +29,7 @@ if (NODE_VERSION.major && NODE_VERSION.major >= 16) { * Initialize Sentry for Node. */ export function init(options: NodeExperimentalOptions | undefined = {}): void { - // Ensure we register our own global hub before something else does - // This will register the NodeExperimentalHub as the global hub - getCurrentHub(); + setupGlobalHub(); const isTracingEnabled = hasTracingEnabled(options); @@ -44,11 +42,11 @@ export function init(options: NodeExperimentalOptions | undefined = {}): void { ]; options.instrumenter = 'otel'; - options.clientClass = NodeExperimentalClient; + options.clientClass = NodeExperimentalClient as unknown as typeof NodeClient; initNode(options); // Always init Otel, even if tracing is disabled, because we need it for trace propagation & the HTTP integration initOtel(); - setOtelContextAsyncContextStrategy(); + setOpenTelemetryContextAsyncContextStrategy(); } diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index b60dac87aeda..271135728186 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -1,17 +1,20 @@ import { diag, DiagLogLevel } from '@opentelemetry/api'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; +import { + getCurrentHub, + SentryPropagator, + SentrySampler, + setupEventContextTrace, + wrapContextManagerClass, +} from '@sentry/opentelemetry'; import { logger } from '@sentry/utils'; -import { SentryPropagator } from '../opentelemetry/propagator'; -import { SentrySampler } from '../opentelemetry/sampler'; -import { SentrySpanProcessor } from '../opentelemetry/spanProcessor'; import type { NodeExperimentalClient } from '../types'; -import { setupEventContextTrace } from '../utils/setupEventContextTrace'; -import { SentryContextManager } from './../opentelemetry/contextManager'; -import { getCurrentHub } from './hub'; +import { NodeExperimentalSentrySpanProcessor } from './spanProcessor'; /** * Initialize OpenTelemetry for Node. @@ -56,15 +59,14 @@ export function setupOtel(client: NodeExperimentalClient): BasicTracerProvider { }), forceFlushTimeoutMillis: 500, }); - provider.addSpanProcessor(new SentrySpanProcessor()); + provider.addSpanProcessor(new NodeExperimentalSentrySpanProcessor()); - // We use a custom context manager to keep context in sync with sentry scope - const contextManager = new SentryContextManager(); + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); // Initialize the provider provider.register({ propagator: new SentryPropagator(), - contextManager, + contextManager: new SentryContextManager(), }); return provider; diff --git a/packages/node-experimental/src/sdk/spanProcessor.ts b/packages/node-experimental/src/sdk/spanProcessor.ts new file mode 100644 index 000000000000..4d120b1e80e6 --- /dev/null +++ b/packages/node-experimental/src/sdk/spanProcessor.ts @@ -0,0 +1,39 @@ +import { SpanKind } from '@opentelemetry/api'; +import type { Span } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { getCurrentHub, SentrySpanProcessor } from '@sentry/opentelemetry'; + +import { Http } from '../integrations/http'; +import { NodeFetch } from '../integrations/node-fetch'; +import type { NodeExperimentalClient } from '../types'; + +/** + * Implement custom code to avoid sending spans in certain cases. + */ +export class NodeExperimentalSentrySpanProcessor extends SentrySpanProcessor { + /** @inheritDoc */ + protected _shouldSendSpanToSentry(span: Span): boolean { + const client = getCurrentHub().getClient(); + const httpIntegration = client ? client.getIntegration(Http) : undefined; + const fetchIntegration = client ? client.getIntegration(NodeFetch) : undefined; + + // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation + // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, + // So we can generate a breadcrumb for it but no span will be sent + if ( + httpIntegration && + (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && + span.attributes[SemanticAttributes.HTTP_URL] && + span.attributes[SemanticAttributes.HTTP_METHOD] + ) { + const shouldCreateSpansForRequests = + span.attributes['http.client'] === 'fetch' + ? fetchIntegration?.shouldCreateSpansForRequests + : httpIntegration?.shouldCreateSpansForRequests; + + return shouldCreateSpansForRequests !== false; + } + + return true; + } +} diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts index 8878a5fd2a8c..70384ddbd3f8 100644 --- a/packages/node-experimental/src/types.ts +++ b/packages/node-experimental/src/types.ts @@ -1,25 +1,15 @@ -import type { Span as WriteableSpan, Tracer } from '@opentelemetry/api'; -import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; +import type { Span as WriteableSpan } from '@opentelemetry/api'; +import type { ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; +import type { OpenTelemetryClient } from '@sentry/opentelemetry'; export type NodeExperimentalOptions = NodeOptions; export type NodeExperimentalClientOptions = ConstructorParameters[0]; -export interface NodeExperimentalClient extends NodeClient { - tracer: Tracer; - traceProvider: BasicTracerProvider | undefined; +export interface NodeExperimentalClient extends NodeClient, OpenTelemetryClient { getOptions(): NodeExperimentalClientOptions; } -export interface NodeExperimentalSpanContext { - name: string; - op?: string; - metadata?: Partial; - origin?: SpanOrigin; - source?: TransactionSource; -} - /** * The base `Span` type is basically a `WriteableSpan`. * There are places where we basically want to allow passing _any_ span, diff --git a/packages/node-experimental/src/utils/addOriginToSpan.ts b/packages/node-experimental/src/utils/addOriginToSpan.ts index 007f55bb1e05..10fb7cf3402f 100644 --- a/packages/node-experimental/src/utils/addOriginToSpan.ts +++ b/packages/node-experimental/src/utils/addOriginToSpan.ts @@ -1,9 +1,8 @@ import type { Span } from '@opentelemetry/api'; +import { _INTERNAL } from '@sentry/opentelemetry'; import type { SpanOrigin } from '@sentry/types'; -import { OTEL_ATTR_ORIGIN } from '../constants'; - /** Adds an origin to an OTEL Span. */ export function addOriginToSpan(span: Span, origin: SpanOrigin): void { - span.setAttribute(OTEL_ATTR_ORIGIN, origin); + _INTERNAL.addOriginToSpan(span, origin); } diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node-experimental/test/integration/breadcrumbs.test.ts index fbd46a6bd466..80842451c3bf 100644 --- a/packages/node-experimental/test/integration/breadcrumbs.test.ts +++ b/packages/node-experimental/test/integration/breadcrumbs.test.ts @@ -1,7 +1,7 @@ -import { withScope } from '../../src/'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; -import { startSpan } from '../../src/sdk/trace'; +import { withScope } from '@sentry/core'; +import { getCurrentHub, startSpan } from '@sentry/opentelemetry'; + +import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; describe('Integration | breadcrumbs', () => { @@ -21,9 +21,6 @@ describe('Integration | breadcrumbs', () => { const hub = getCurrentHub(); const client = hub.getClient() as NodeExperimentalClient; - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(client).toBeInstanceOf(NodeExperimentalClient); - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); @@ -61,9 +58,6 @@ describe('Integration | breadcrumbs', () => { const hub = getCurrentHub(); const client = hub.getClient() as NodeExperimentalClient; - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(client).toBeInstanceOf(NodeExperimentalClient); - const error = new Error('test'); hub.addBreadcrumb({ timestamp: 123456, message: 'test0' }); @@ -328,11 +322,11 @@ describe('Integration | breadcrumbs', () => { const promise2 = startSpan({ name: 'test-b' }, async () => { hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); - await startSpan({ name: 'inner1' }, async () => { + await startSpan({ name: 'inner1b' }, async () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); - await startSpan({ name: 'inner2' }, async () => { + await startSpan({ name: 'inner2b' }, async () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); }); }); diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node-experimental/test/integration/scope.test.ts index 925047583f2e..5beb1b7ec06f 100644 --- a/packages/node-experimental/test/integration/scope.test.ts +++ b/packages/node-experimental/test/integration/scope.test.ts @@ -1,7 +1,7 @@ +import { getCurrentHub, getSpanScope } from '@sentry/opentelemetry'; + import * as Sentry from '../../src/'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; +import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; describe('Integration | Scope', () => { @@ -24,10 +24,6 @@ describe('Integration | Scope', () => { const rootScope = hub.getScope(); - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(rootScope).toBeInstanceOf(NodeExperimentalScope); - expect(client).toBeInstanceOf(NodeExperimentalClient); - const error = new Error('test error'); let spanId: string | undefined; let traceId: string | undefined; @@ -45,8 +41,10 @@ describe('Integration | Scope', () => { scope2.setTag('tag3', 'val3'); Sentry.startSpan({ name: 'outer' }, span => { - spanId = span?.spanContext().spanId; - traceId = span?.spanContext().traceId; + expect(getSpanScope(span)).toBe(enableTracing ? scope2 : undefined); + + spanId = span.spanContext().spanId; + traceId = span.spanContext().traceId; Sentry.setTag('tag4', 'val4'); @@ -96,7 +94,6 @@ describe('Integration | Scope', () => { trace_id: traceId, }, }), - spans: [], start_timestamp: expect.any(Number), tags: { @@ -127,10 +124,6 @@ describe('Integration | Scope', () => { const rootScope = hub.getScope(); - expect(hub).toBeInstanceOf(NodeExperimentalHub); - expect(rootScope).toBeInstanceOf(NodeExperimentalScope); - expect(client).toBeInstanceOf(NodeExperimentalClient); - const error1 = new Error('test error 1'); const error2 = new Error('test error 2'); let spanId1: string | undefined; @@ -147,8 +140,8 @@ describe('Integration | Scope', () => { scope2.setTag('tag3', 'val3a'); Sentry.startSpan({ name: 'outer' }, span => { - spanId1 = span?.spanContext().spanId; - traceId1 = span?.spanContext().traceId; + spanId1 = span.spanContext().spanId; + traceId1 = span.spanContext().traceId; Sentry.setTag('tag4', 'val4a'); @@ -164,8 +157,8 @@ describe('Integration | Scope', () => { scope2.setTag('tag3', 'val3b'); Sentry.startSpan({ name: 'outer' }, span => { - spanId2 = span?.spanContext().spanId; - traceId2 = span?.spanContext().traceId; + spanId2 = span.spanContext().spanId; + traceId2 = span.spanContext().traceId; Sentry.setTag('tag4', 'val4b'); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 4d657fc4cbf5..2f6cea23c0fd 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,16 +1,14 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { getCurrentHub, SentrySpanProcessor, setPropagationContextOnContext } from '@sentry/opentelemetry'; import type { Integration, PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; import * as Sentry from '../../src'; import { startSpan } from '../../src'; -import { SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY } from '../../src/constants'; import type { Http, NodeFetch } from '../../src/integrations'; -import { SentrySpanProcessor } from '../../src/opentelemetry/spanProcessor'; -import type { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; +import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; describe('Integration | Transactions', () => { @@ -362,10 +360,7 @@ describe('Integration | Transactions', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { Sentry.startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { if (!span) { diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 8087d8b7a29f..f49b31fdee7a 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -35,10 +35,10 @@ }, "devDependencies": { "@opentelemetry/api": "^1.6.0", - "@opentelemetry/core": "^1.7.0", - "@opentelemetry/sdk-trace-base": "^1.17.0", - "@opentelemetry/sdk-trace-node": "^1.17.0", - "@opentelemetry/semantic-conventions": "^1.17.0", + "@opentelemetry/core": "^1.17.1", + "@opentelemetry/sdk-trace-base": "^1.17.1", + "@opentelemetry/sdk-trace-node": "^1.17.1", + "@opentelemetry/semantic-conventions": "^1.17.1", "@sentry/node": "7.74.1" }, "scripts": { diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index ea6e0833a395..fff3d0f4bc98 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -275,7 +275,7 @@ describe('SentrySpanProcessor', () => { 'service.name': 'test-service', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.17.0', + 'telemetry.sdk.version': '1.17.1', }, }, }); @@ -300,7 +300,7 @@ describe('SentrySpanProcessor', () => { 'service.name': 'test-service', 'telemetry.sdk.language': 'nodejs', 'telemetry.sdk.name': 'opentelemetry', - 'telemetry.sdk.version': '1.17.0', + 'telemetry.sdk.version': '1.17.1', }, }, }); diff --git a/packages/opentelemetry/.eslintrc.js b/packages/opentelemetry/.eslintrc.js new file mode 100644 index 000000000000..9899ea1b73d8 --- /dev/null +++ b/packages/opentelemetry/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + }, +}; diff --git a/packages/opentelemetry/LICENSE b/packages/opentelemetry/LICENSE new file mode 100644 index 000000000000..d11896ba1181 --- /dev/null +++ b/packages/opentelemetry/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md new file mode 100644 index 000000000000..2a5c88fa85cc --- /dev/null +++ b/packages/opentelemetry/README.md @@ -0,0 +1,98 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for OpenTelemetry + +[![npm version](https://img.shields.io/npm/v/@sentry/opentelemetry.svg)](https://www.npmjs.com/package/@sentry/opentelemetry) +[![npm dm](https://img.shields.io/npm/dm/@sentry/opentelemetry.svg)](https://www.npmjs.com/package/@sentry/opentelemetry) +[![npm dt](https://img.shields.io/npm/dt/@sentry/opentelemetry.svg)](https://www.npmjs.com/package/@sentry/opentelemetry) + +This package allows you to send your OpenTelemetry trace data to Sentry via OpenTelemetry SpanProcessors. + +This SDK is **considered experimental and in an alpha state**. It may experience breaking changes. Please reach out on +[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback/concerns. + +## Installation + +```bash +npm install @sentry/opentelemetry + +# Or yarn +yarn add @sentry/opentelemetry +``` + +Note that `@sentry/opentelemetry` depends on the following peer dependencies: + +- `@opentelemetry/api` version `1.0.0` or greater +- `@opentelemetry/core` version `1.0.0` or greater +- `@opentelemetry/semantic-conventions` version `1.0.0` or greater +- `@opentelemetry/sdk-trace-base` version `1.0.0` or greater, or a package that implements that, like + `@opentelemetry/sdk-node`. + +## Usage + +This package exposes a few building blocks you can add to your OpenTelemetry setup in order to capture OpenTelemetry traces to Sentry. + +This is how you can use this in your app: + +1. Setup the global hub for OpenTelemetry compatibility - ensure `setupGlobalHub()` is called before anything else! +1. Initialize Sentry, e.g. `@sentry/node` - make sure to set `instrumenter: 'otel'` in the SDK `init({})`! +1. Call `setupEventContextTrace(client)` +1. Add `SentrySampler` as sampler +1. Add `SentrySpanProcessor` as span processor +1. Add a context manager wrapped via `wrapContextManagerClass` +1. Add `SentryPropagator` as propagator +1. Setup OTEL-powered async context strategy for Sentry via `setOpenTelemetryContextAsyncContextStrategy()` + +For example, you could set this up as follows: + +```js +import * as Sentry from '@sentry/node'; +import { + getCurrentHub, + setupGlobalHub, + SentryPropagator, + SentrySampler, + SentrySpanProcessor, + setupEventContextTrace, + wrapContextManagerClass, + setOpenTelemetryContextAsyncContextStrategy, +} from '@sentry/opentelemetry'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; + +function setupSentry() { + setupGlobalHub(); + + Sentry.init({ + dsn: 'xxx', + instrumenter: 'otel' + }); + + const client = getCurrentHub().getClient(); + setupEventContextTrace(client); + + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + }); + provider.addSpanProcessor(new SentrySpanProcessor()); + + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + + // Initialize the provider + provider.register({ + propagator: new SentryPropagator(), + contextManager: new SentryContextManager(), + }); + + setOpenTelemetryContextAsyncContextStrategy(); +} +``` + +A full setup example can be found in (node-experimental)[./../node-experimental]. + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/opentelemetry/jest.config.js b/packages/opentelemetry/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/opentelemetry/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json new file mode 100644 index 000000000000..a3064234dbac --- /dev/null +++ b/packages/opentelemetry/package.json @@ -0,0 +1,73 @@ +{ + "name": "@sentry/opentelemetry", + "version": "7.74.1", + "description": "Official Sentry utilities for OpenTelemetry", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "typesVersions": { + "<4.9": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "7.74.1", + "@sentry/types": "7.74.1", + "@sentry/utils": "7.74.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/core": "^1.0.0", + "@opentelemetry/sdk-trace-base": "^1.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0" + }, + "devDependencies": { + "@opentelemetry/api": "^1.6.0", + "@opentelemetry/context-async-hooks": "^1.17.1", + "@opentelemetry/core": "^1.17.1", + "@opentelemetry/sdk-trace-base": "^1.17.1", + "@opentelemetry/sdk-trace-node": "^1.17.1", + "@opentelemetry/semantic-conventions": "^1.17.1" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.js", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build coverage sentry-opentelemetry-*.tgz", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "eslint . --format stylish --fix", + "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", + "lint": "run-s lint:prettier lint:eslint", + "lint:eslint": "eslint . --format stylish", + "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", + "test": "yarn test:jest", + "test:jest": "jest", + "test:watch": "jest --watch", + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/opentelemetry/rollup.npm.config.js b/packages/opentelemetry/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/opentelemetry/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/node-experimental/src/sdk/otelAsyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts similarity index 82% rename from packages/node-experimental/src/sdk/otelAsyncContextStrategy.ts rename to packages/opentelemetry/src/asyncContextStrategy.ts index 7e4ca5cd4da0..c4cc48c1cfb5 100644 --- a/packages/node-experimental/src/sdk/otelAsyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -2,18 +2,19 @@ import * as api from '@opentelemetry/api'; import type { Hub, RunWithAsyncContextOptions } from '@sentry/core'; import { setAsyncContextStrategy } from '@sentry/core'; -import { OTEL_CONTEXT_HUB_KEY } from '../constants'; +import { getHubFromContext } from './utils/contextData'; /** * Sets the async context strategy to use follow the OTEL context under the hood. * We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts) */ -export function setOtelContextAsyncContextStrategy(): void { +export function setOpenTelemetryContextAsyncContextStrategy(): void { function getCurrentHub(): Hub | undefined { const ctx = api.context.active(); // Returning undefined means the global hub will be used - return ctx.getValue(OTEL_CONTEXT_HUB_KEY) as Hub | undefined; + // Need to cast from @sentry/type's `Hub` to @sentry/core's `Hub` + return getHubFromContext(ctx) as Hub | undefined; } /* This is more or less a NOOP - we rely on the OTEL context manager for this */ diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts new file mode 100644 index 000000000000..36c8a36f886c --- /dev/null +++ b/packages/opentelemetry/src/constants.ts @@ -0,0 +1,10 @@ +import { createContextKey } from '@opentelemetry/api'; + +export const SENTRY_TRACE_HEADER = 'sentry-trace'; +export const SENTRY_BAGGAGE_HEADER = 'baggage'; + +/** Context Key to hold a PropagationContext. */ +export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY'); + +/** Context Key to hold a Hub. */ +export const SENTRY_HUB_CONTEXT_KEY = createContextKey('sentry_hub'); diff --git a/packages/opentelemetry/src/contextManager.ts b/packages/opentelemetry/src/contextManager.ts new file mode 100644 index 000000000000..ca9305dfea9b --- /dev/null +++ b/packages/opentelemetry/src/contextManager.ts @@ -0,0 +1,57 @@ +import type { Context, ContextManager } from '@opentelemetry/api'; +import type { Carrier, Hub } from '@sentry/core'; + +import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './custom/hub'; +import { setHubOnContext } from './utils/contextData'; + +function createNewHub(parent: Hub | undefined): Hub { + const carrier: Carrier = {}; + ensureHubOnCarrier(carrier, parent); + return getHubFromCarrier(carrier); +} + +// Typescript complains if we do not use `...args: any[]` for the mixin, with: +// A mixin class must have a constructor with a single rest parameter of type 'any[]'.ts(2545) +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Wrap an OpenTelemetry ContextManager in a way that ensures the context is kept in sync with the Sentry Hub. + * + * Usage: + * import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; + * const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + * const contextManager = new SentryContextManager(); + */ +export function wrapContextManagerClass( + ContextManagerClass: new (...args: any[]) => ContextManagerInstance, +): typeof ContextManagerClass { + /** + * This is a custom ContextManager for OpenTelemetry, which extends the default AsyncLocalStorageContextManager. + * It ensures that we create a new hub per context, so that the OTEL Context & the Sentry Hub are always in sync. + * + * Note that we currently only support AsyncHooks with this, + * but since this should work for Node 14+ anyhow that should be good enough. + */ + + // @ts-expect-error TS does not like this, but we know this is fine + class SentryContextManager extends ContextManagerClass { + /** + * Overwrite with() of the original AsyncLocalStorageContextManager + * to ensure we also create a new hub per context. + */ + public with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const existingHub = getCurrentHub(); + const newHub = createNewHub(existingHub); + + return super.with(setHubOnContext(context, newHub), fn, thisArg, ...args); + } + } + + return SentryContextManager as unknown as typeof ContextManagerClass; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/opentelemetry/src/custom/client.ts b/packages/opentelemetry/src/custom/client.ts new file mode 100644 index 000000000000..adf348da60e1 --- /dev/null +++ b/packages/opentelemetry/src/custom/client.ts @@ -0,0 +1,81 @@ +import type { Tracer } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { BaseClient, Scope } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; +import type { Client, Event, EventHint } from '@sentry/types'; + +import type { OpenTelemetryClient as OpenTelemetryClientInterface } from '../types'; +import { OpenTelemetryScope } from './scope'; + +// Typescript complains if we do not use `...args: any[]` for the mixin, with: +// A mixin class must have a constructor with a single rest parameter of type 'any[]'.ts(2545) +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Wrap an Client with things we need for OpenTelemetry support. + * + * Usage: + * const OpenTelemetryClient = getWrappedClientClass(NodeClient); + * const client = new OpenTelemetryClient(options); + */ +export function wrapClientClass< + ClassConstructor extends new (...args: any[]) => Client & BaseClient, + WrappedClassConstructor extends new (...args: any[]) => Client & BaseClient & OpenTelemetryClientInterface, +>(ClientClass: ClassConstructor): WrappedClassConstructor { + class OpenTelemetryClient extends ClientClass implements OpenTelemetryClientInterface { + public traceProvider: BasicTracerProvider | undefined; + private _tracer: Tracer | undefined; + + public constructor(...args: any[]) { + super(...args); + } + + /** Get the OTEL tracer. */ + public get tracer(): Tracer { + if (this._tracer) { + return this._tracer; + } + + const name = '@sentry/opentelemetry'; + const version = SDK_VERSION; + const tracer = trace.getTracer(name, version); + this._tracer = tracer; + + return tracer; + } + + /** + * @inheritDoc + */ + public async flush(timeout?: number): Promise { + const provider = this.traceProvider; + const spanProcessor = provider?.activeSpanProcessor; + + if (spanProcessor) { + await spanProcessor.forceFlush(); + } + + return super.flush(timeout); + } + + /** + * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. + * This uses `Scope.clone()`, which we need to replace with `NodeExperimentalScope.clone()` for this client. + */ + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + let actualScope = scope; + + // Remove `captureContext` hint and instead clone already here + if (hint && hint.captureContext) { + actualScope = OpenTelemetryScope.clone(scope); + delete hint.captureContext; + } + + return super._prepareEvent(event, hint, actualScope); + } + } + + return OpenTelemetryClient as unknown as WrappedClassConstructor; +} +/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/opentelemetry/src/custom/hub.ts similarity index 83% rename from packages/node-experimental/src/sdk/hub.ts rename to packages/opentelemetry/src/custom/hub.ts index 50958d13c84d..46dccd3c86e4 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/opentelemetry/src/custom/hub.ts @@ -3,14 +3,14 @@ import { Hub } from '@sentry/core'; import type { Client } from '@sentry/types'; import { getGlobalSingleton, GLOBAL_OBJ } from '@sentry/utils'; -import { NodeExperimentalScope } from './scope'; +import { OpenTelemetryScope } from './scope'; /** * A custom hub that ensures we always creat an OTEL scope. * Exported only for testing */ -export class NodeExperimentalHub extends Hub { - public constructor(client?: Client, scope: Scope = new NodeExperimentalScope()) { +export class OpenTelemetryHub extends Hub { + public constructor(client?: Client, scope: Scope = new OpenTelemetryScope()) { super(client, scope); } @@ -19,7 +19,7 @@ export class NodeExperimentalHub extends Hub { */ public pushScope(): Scope { // We want to clone the content of prev scope - const scope = NodeExperimentalScope.clone(this.getScope()); + const scope = OpenTelemetryScope.clone(this.getScope()); this.getStack().push({ client: this.getClient(), scope, @@ -72,6 +72,20 @@ export function getCurrentHub(): Hub { return getGlobalHub(registry); } +/** + * Ensure the global hub is an OpenTelemetryHub. + */ +export function setupGlobalHub(): void { + const globalRegistry = getMainCarrier(); + + if (getGlobalHub(globalRegistry) instanceof OpenTelemetryHub) { + return; + } + + // If the current global hub is not correct, ensure we overwrite it + setHubOnCarrier(globalRegistry, new OpenTelemetryHub()); +} + /** * This will create a new {@link Hub} and add to the passed object on * __SENTRY__.hub. @@ -79,7 +93,7 @@ export function getCurrentHub(): Hub { * @hidden */ export function getHubFromCarrier(carrier: Carrier): Hub { - return getGlobalSingleton('hub', () => new NodeExperimentalHub(), carrier); + return getGlobalSingleton('hub', () => new OpenTelemetryHub(), carrier); } /** @@ -93,7 +107,7 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( const globalHubTopStack = parent.getStackTop(); setHubOnCarrier( carrier, - new NodeExperimentalHub(globalHubTopStack.client, NodeExperimentalScope.clone(globalHubTopStack.scope)), + new OpenTelemetryHub(globalHubTopStack.client, OpenTelemetryScope.clone(globalHubTopStack.scope)), ); } } @@ -101,7 +115,7 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { - setHubOnCarrier(registry, new NodeExperimentalHub()); + setHubOnCarrier(registry, new OpenTelemetryHub()); } // Return hub that lives on a global object diff --git a/packages/node-experimental/src/sdk/hubextensions.ts b/packages/opentelemetry/src/custom/hubextensions.ts similarity index 90% rename from packages/node-experimental/src/sdk/hubextensions.ts rename to packages/opentelemetry/src/custom/hubextensions.ts index 07ee08c1f7f9..4e839a9f3314 100644 --- a/packages/node-experimental/src/sdk/hubextensions.ts +++ b/packages/opentelemetry/src/custom/hubextensions.ts @@ -23,7 +23,7 @@ function startTransactionNoop( _customSamplingContext?: CustomSamplingContext, ): unknown { // eslint-disable-next-line no-console - console.warn('startTransaction is a noop in @sentry/node-experimental. Use `startSpan` instead.'); + console.warn('startTransaction is a noop in @sentry/opentelemetry. Use `startSpan` instead.'); // We return an object here as hub.ts checks for the result of this // and renders a different warning if this is empty return {}; diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/opentelemetry/src/custom/scope.ts similarity index 77% rename from packages/node-experimental/src/sdk/scope.ts rename to packages/opentelemetry/src/custom/scope.ts index 39f931936ccf..9c544b018134 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -4,20 +4,14 @@ import { Scope } from '@sentry/core'; import type { Breadcrumb, SeverityLevel, Span as SentrySpan } from '@sentry/types'; import { dateTimestampInSeconds, dropUndefinedKeys, logger, normalize } from '@sentry/utils'; -import { - OTEL_ATTR_BREADCRUMB_CATEGORY, - OTEL_ATTR_BREADCRUMB_DATA, - OTEL_ATTR_BREADCRUMB_EVENT_ID, - OTEL_ATTR_BREADCRUMB_LEVEL, - OTEL_ATTR_BREADCRUMB_TYPE, -} from '../constants'; -import { getSpanParent } from '../opentelemetry/spanData'; +import { InternalSentrySemanticAttributes } from '../semanticAttributes'; import { convertOtelTimeToSeconds } from '../utils/convertOtelTimeToSeconds'; import { getActiveSpan, getRootSpan } from '../utils/getActiveSpan'; +import { getSpanParent } from '../utils/spanData'; import { spanHasEvents } from '../utils/spanTypes'; /** A fork of the classic scope with some otel specific stuff. */ -export class NodeExperimentalScope extends Scope { +export class OpenTelemetryScope extends Scope { /** * This can be set to ensure the scope uses _this_ span as the active one, * instead of using getActiveSpan(). @@ -28,7 +22,7 @@ export class NodeExperimentalScope extends Scope { * @inheritDoc */ public static clone(scope?: Scope): Scope { - const newScope = new NodeExperimentalScope(); + const newScope = new OpenTelemetryScope(); if (scope) { newScope._breadcrumbs = [...scope['_breadcrumbs']]; newScope._tags = { ...scope['_tags'] }; @@ -55,7 +49,7 @@ export class NodeExperimentalScope extends Scope { */ public getSpan(): undefined { __DEBUG_BUILD__ && - logger.warn('Calling getSpan() is a noop in @sentry/node-experimental. Use `getActiveSpan()` instead.'); + logger.warn('Calling getSpan() is a noop in @sentry/opentelemetry. Use `getActiveSpan()` instead.'); return undefined; } @@ -65,8 +59,7 @@ export class NodeExperimentalScope extends Scope { * Instead, use the global `startSpan()` to define the active span. */ public setSpan(_span: SentrySpan): this { - __DEBUG_BUILD__ && - logger.warn('Calling setSpan() is a noop in @sentry/node-experimental. Use `startSpan()` instead.'); + __DEBUG_BUILD__ && logger.warn('Calling setSpan() is a noop in @sentry/opentelemetry. Use `startSpan()` instead.'); return this; } @@ -120,10 +113,10 @@ function breadcrumbToOtelEvent(breadcrumb: Breadcrumb): Parameters = (client && client.getOptions()) || {}; - const transaction = new NodeExperimentalTransaction(transactionContext, hub as Hub); + const transaction = new OpenTelemetryTransaction(transactionContext, hub as Hub); // Since we do not do sampling here, we assume that this is _always_ sampled // Any sampling decision happens in OpenTelemetry's sampler transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); @@ -25,7 +25,7 @@ export function startTransaction(hub: HubInterface, transactionContext: Transact /** * This is a fork of the base Transaction with OTEL specific stuff added. */ -export class NodeExperimentalTransaction extends Transaction { +export class OpenTelemetryTransaction extends Transaction { /** * Finish the transaction, but apply the given scope instead of the current one. */ diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts new file mode 100644 index 000000000000..eb516e03dea1 --- /dev/null +++ b/packages/opentelemetry/src/index.ts @@ -0,0 +1,47 @@ +import { addOriginToSpan } from './utils/addOriginToSpan'; +import { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent'; +import { getRequestSpanData } from './utils/getRequestSpanData'; + +export type { OpenTelemetryClient } from './types'; +export { wrapClientClass } from './custom/client'; + +export { getSpanKind } from './utils/getSpanKind'; +export { getSpanHub, getSpanMetadata, getSpanParent, getSpanScope, setSpanMetadata } from './utils/spanData'; + +export { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData'; + +export { + spanHasAttributes, + spanHasEvents, + spanHasKind, + spanHasName, + spanHasParentId, + spanHasStatus, +} from './utils/spanTypes'; + +export { isSentryRequestSpan } from './utils/isSentryRequest'; + +export { getActiveSpan, getRootSpan } from './utils/getActiveSpan'; +export { startSpan, startInactiveSpan } from './trace'; + +export { getCurrentHub, setupGlobalHub } from './custom/hub'; +export { addTracingExtensions } from './custom/hubextensions'; +export { setupEventContextTrace } from './setupEventContextTrace'; + +export { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrategy'; +export { wrapContextManagerClass } from './contextManager'; +export { SentryPropagator } from './propagator'; +export { SentrySpanProcessor } from './spanProcessor'; +export { SentrySampler } from './sampler'; + +/** + * The following internal utils are not considered public API and are subject to change. + * @hidden + */ +const _INTERNAL = { + addOriginToSpan, + maybeCaptureExceptionForTimedEvent, + getRequestSpanData, +} as const; + +export { _INTERNAL }; diff --git a/packages/node-experimental/src/opentelemetry/propagator.ts b/packages/opentelemetry/src/propagator.ts similarity index 90% rename from packages/node-experimental/src/opentelemetry/propagator.ts rename to packages/opentelemetry/src/propagator.ts index 7aa43271b72c..db7a78d4f8f2 100644 --- a/packages/node-experimental/src/opentelemetry/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -5,9 +5,10 @@ import { getDynamicSamplingContextFromClient } from '@sentry/core'; import type { DynamicSamplingContext, PropagationContext } from '@sentry/types'; import { generateSentryTraceHeader, SENTRY_BAGGAGE_KEY_PREFIX, tracingContextFromHeaders } from '@sentry/utils'; -import { getCurrentHub } from '../sdk/hub'; -import { SENTRY_BAGGAGE_HEADER, SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, SENTRY_TRACE_HEADER } from './../constants'; -import { getSpanScope } from './spanData'; +import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants'; +import { getCurrentHub } from './custom/hub'; +import { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData'; +import { getSpanScope } from './utils/spanData'; /** * Injects and extracts `sentry-trace` and `baggage` headers from carriers. @@ -23,12 +24,8 @@ export class SentryPropagator extends W3CBaggagePropagator { let baggage = propagation.getBaggage(context) || propagation.createBaggage({}); - const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as - | PropagationContext - | undefined; - + const propagationContext = getPropagationContextFromContext(context); const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext); - const dynamicSamplingContext = propagationContext ? getDsc(context, propagationContext, traceId) : undefined; if (dynamicSamplingContext) { @@ -61,7 +58,7 @@ export class SentryPropagator extends W3CBaggagePropagator { const { propagationContext } = tracingContextFromHeaders(sentryTraceHeader, maybeBaggageHeader); // Add propagation context to context - const contextWithPropagationContext = context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); + const contextWithPropagationContext = setPropagationContextOnContext(context, propagationContext); const spanContext: SpanContext = { traceId: propagationContext.traceId, diff --git a/packages/node-experimental/src/opentelemetry/sampler.ts b/packages/opentelemetry/src/sampler.ts similarity index 90% rename from packages/node-experimental/src/opentelemetry/sampler.ts rename to packages/opentelemetry/src/sampler.ts index 373c3b314b70..03f82f100a87 100644 --- a/packages/node-experimental/src/opentelemetry/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -4,14 +4,11 @@ import { isSpanContextValid, trace, TraceFlags } from '@opentelemetry/api'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { hasTracingEnabled } from '@sentry/core'; -import type { Client, ClientOptions, PropagationContext, SamplingContext } from '@sentry/types'; +import type { Client, ClientOptions, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; -import { - OTEL_ATTR_PARENT_SAMPLED, - OTEL_ATTR_SENTRY_SAMPLE_RATE, - SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, -} from '../constants'; +import { InternalSentrySemanticAttributes } from './semanticAttributes'; +import { getPropagationContextFromContext } from './utils/contextData'; /** * A custom OTEL sampler that uses Sentry sampling rates to make it's decision @@ -65,11 +62,11 @@ export class SentrySampler implements Sampler { }); const attributes: Attributes = { - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: Number(sampleRate), + [InternalSentrySemanticAttributes.SAMPLE_RATE]: Number(sampleRate), }; if (typeof parentSampled === 'boolean') { - attributes[OTEL_ATTR_PARENT_SAMPLED] = parentSampled; + attributes[InternalSentrySemanticAttributes.PARENT_SAMPLED] = parentSampled; } // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The @@ -180,13 +177,9 @@ function isValidSampleRate(rate: unknown): boolean { return true; } -function getPropagationContext(parentContext: Context): PropagationContext | undefined { - return parentContext.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined; -} - function getParentRemoteSampled(spanContext: SpanContext, context: Context): boolean | undefined { const traceId = spanContext.traceId; - const traceparentData = getPropagationContext(context); + const traceparentData = getPropagationContextFromContext(context); // Only inherit sample rate if `traceId` is the same return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined; diff --git a/packages/opentelemetry/src/semanticAttributes.ts b/packages/opentelemetry/src/semanticAttributes.ts new file mode 100644 index 000000000000..00c37f061079 --- /dev/null +++ b/packages/opentelemetry/src/semanticAttributes.ts @@ -0,0 +1,16 @@ +/** + * These are internal and are not supposed to be used/depended on by external parties. + * No guarantees apply to these attributes, and the may change/disappear at any time. + */ +export const InternalSentrySemanticAttributes = { + ORIGIN: 'sentry.origin', + OP: 'sentry.op', + SOURCE: 'sentry.source', + SAMPLE_RATE: 'sentry.sample_rate', + PARENT_SAMPLED: 'sentry.parentSampled', + BREADCRUMB_TYPE: 'sentry.breadcrumb.type', + BREADCRUMB_LEVEL: 'sentry.breadcrumb.level', + BREADCRUMB_EVENT_ID: 'sentry.breadcrumb.event_id', + BREADCRUMB_CATEGORY: 'sentry.breadcrumb.category', + BREADCRUMB_DATA: 'sentry.breadcrumb.data', +} as const; diff --git a/packages/node-experimental/src/utils/setupEventContextTrace.ts b/packages/opentelemetry/src/setupEventContextTrace.ts similarity index 86% rename from packages/node-experimental/src/utils/setupEventContextTrace.ts rename to packages/opentelemetry/src/setupEventContextTrace.ts index 0e8dc7c23d7b..c55fc27ed52f 100644 --- a/packages/node-experimental/src/utils/setupEventContextTrace.ts +++ b/packages/opentelemetry/src/setupEventContextTrace.ts @@ -1,7 +1,7 @@ import type { Client } from '@sentry/types'; -import { getActiveSpan } from './getActiveSpan'; -import { spanHasParentId } from './spanTypes'; +import { getActiveSpan } from './utils/getActiveSpan'; +import { spanHasParentId } from './utils/spanTypes'; /** Ensure the `trace` context is set on all events. */ export function setupEventContextTrace(client: Client): void { diff --git a/packages/node-experimental/src/opentelemetry/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts similarity index 79% rename from packages/node-experimental/src/opentelemetry/spanExporter.ts rename to packages/opentelemetry/src/spanExporter.ts index e242f74d6104..f2094d132733 100644 --- a/packages/node-experimental/src/opentelemetry/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -2,29 +2,24 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; -import type { ReadableSpan, Span as SdkTraceBaseSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; +import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { flush } from '@sentry/core'; -import { mapOtelStatus, parseOtelSpanDescription } from '@sentry/opentelemetry-node'; import type { DynamicSamplingContext, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { - OTEL_ATTR_OP, - OTEL_ATTR_ORIGIN, - OTEL_ATTR_PARENT_SAMPLED, - OTEL_ATTR_SENTRY_SAMPLE_RATE, - OTEL_ATTR_SOURCE, -} from '../constants'; -import { getCurrentHub } from '../sdk/hub'; -import { NodeExperimentalScope } from '../sdk/scope'; -import type { NodeExperimentalTransaction } from '../sdk/transaction'; -import { startTransaction } from '../sdk/transaction'; -import { convertOtelTimeToSeconds } from '../utils/convertOtelTimeToSeconds'; -import { getRequestSpanData } from '../utils/getRequestSpanData'; -import type { SpanNode } from '../utils/groupSpansWithParents'; -import { groupSpansWithParents } from '../utils/groupSpansWithParents'; -import { getSpanHub, getSpanMetadata, getSpanScope } from './spanData'; +import { getCurrentHub } from './custom/hub'; +import { OpenTelemetryScope } from './custom/scope'; +import type { OpenTelemetryTransaction } from './custom/transaction'; +import { startTransaction } from './custom/transaction'; +import { InternalSentrySemanticAttributes } from './semanticAttributes'; +import { convertOtelTimeToSeconds } from './utils/convertOtelTimeToSeconds'; +import { getRequestSpanData } from './utils/getRequestSpanData'; +import type { SpanNode } from './utils/groupSpansWithParents'; +import { groupSpansWithParents } from './utils/groupSpansWithParents'; +import { mapStatus } from './utils/mapStatus'; +import { parseSpanDescription } from './utils/parseSpanDescription'; +import { getSpanHub, getSpanMetadata, getSpanScope } from './utils/spanData'; type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; @@ -117,9 +112,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { // Now finish the transaction, which will send it together with all the spans // We make sure to use the current span as the activeSpan for this transaction const scope = getSpanScope(span); - const forkedScope = NodeExperimentalScope.clone( - scope as NodeExperimentalScope | undefined, - ) as NodeExperimentalScope; + const forkedScope = OpenTelemetryScope.clone(scope as OpenTelemetryScope | undefined) as OpenTelemetryScope; forkedScope.activeSpan = span as unknown as Span; transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), forkedScope); @@ -142,14 +135,14 @@ function shouldCleanupSpan(span: ReadableSpan, maxStartTimeOffsetSeconds: number function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { const attributes = span.attributes; - const origin = attributes[OTEL_ATTR_ORIGIN] as SpanOrigin | undefined; - const op = attributes[OTEL_ATTR_OP] as string | undefined; - const source = attributes[OTEL_ATTR_SOURCE] as TransactionSource | undefined; + const origin = attributes[InternalSentrySemanticAttributes.ORIGIN] as SpanOrigin | undefined; + const op = attributes[InternalSentrySemanticAttributes.OP] as string | undefined; + const source = attributes[InternalSentrySemanticAttributes.SOURCE] as TransactionSource | undefined; return { origin, op, source }; } -function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTransaction { +function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransaction { const scope = getSpanScope(span); const hub = getSpanHub(span) || getCurrentHub(); const spanContext = span.spanContext(); @@ -157,12 +150,12 @@ function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTrans const traceId = spanContext.traceId; const parentSpanId = span.parentSpanId; - const parentSampled = span.attributes[OTEL_ATTR_PARENT_SAMPLED] as boolean | undefined; + const parentSampled = span.attributes[InternalSentrySemanticAttributes.PARENT_SAMPLED] as boolean | undefined; const dynamicSamplingContext: DynamicSamplingContext | undefined = scope ? scope.getPropagationContext().dsc : undefined; - const { op, description, tags, data, origin, source } = getSpanData(span as SdkTraceBaseSpan); + const { op, description, tags, data, origin, source } = getSpanData(span); const metadata = getSpanMetadata(span); const transaction = startTransaction(hub, { @@ -173,19 +166,19 @@ function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTrans name: description, op, instrumenter: 'otel', - status: mapOtelStatus(span as SdkTraceBaseSpan), + status: mapStatus(span), startTimestamp: convertOtelTimeToSeconds(span.startTime), metadata: { dynamicSamplingContext, source, - sampleRate: span.attributes[OTEL_ATTR_SENTRY_SAMPLE_RATE] as number | undefined, + sampleRate: span.attributes[InternalSentrySemanticAttributes.SAMPLE_RATE] as number | undefined, ...metadata, }, data: removeSentryAttributes(data), origin, tags, sampled: true, - }) as NodeExperimentalTransaction; + }) as OpenTelemetryTransaction; transaction.setContext('otel', { attributes: removeSentryAttributes(span.attributes), @@ -212,14 +205,14 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: Sentry const spanId = span.spanContext().spanId; const { attributes } = span; - const { op, description, tags, data, origin } = getSpanData(span as SdkTraceBaseSpan); + const { op, description, tags, data, origin } = getSpanData(span); const allData = { ...removeSentryAttributes(attributes), ...data }; const sentrySpan = sentryParentSpan.startChild({ description, op, data: allData, - status: mapOtelStatus(span as SdkTraceBaseSpan), + status: mapStatus(span), instrumenter: 'otel', startTimestamp: convertOtelTimeToSeconds(span.startTime), spanId, @@ -243,12 +236,7 @@ function getSpanData(span: ReadableSpan): { origin?: SpanOrigin; } { const { op: definedOp, source: definedSource, origin } = parseSpan(span); - const { - op: inferredOp, - description, - source: inferredSource, - data: inferredData, - } = parseOtelSpanDescription(span as SdkTraceBaseSpan); + const { op: inferredOp, description, source: inferredSource, data: inferredData } = parseSpanDescription(span); const op = definedOp || inferredOp; const source = definedSource || inferredSource; @@ -274,11 +262,11 @@ function removeSentryAttributes(data: Record): Record { + maybeCaptureExceptionForTimedEvent(hub, event, span); + }); +} + +/** + * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via + * the Sentry SDK. + */ +export class SentrySpanProcessor extends BatchSpanProcessor implements SpanProcessorInterface { + public constructor() { + super(new SentrySpanExporter()); + } + + /** + * @inheritDoc + */ + public onStart(span: Span, parentContext: Context): void { + onSpanStart(span, parentContext); + + __DEBUG_BUILD__ && logger.log(`[Tracing] Starting span "${span.name}" (${span.spanContext().spanId})`); + + return super.onStart(span, parentContext); + } + + /** @inheritDoc */ + public onEnd(span: Span): void { + __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing span "${span.name}" (${span.spanContext().spanId})`); + + if (!this._shouldSendSpanToSentry(span)) { + // Prevent this being called to super.onEnd(), which would pass this to the span exporter + return; + } + + onSpanEnd(span); + + return super.onEnd(span); + } + + /** + * You can overwrite this in a sub class to implement custom behavior for dropping spans. + * If you return `false` here, the span will not be passed to the exporter and thus not be sent. + */ + protected _shouldSendSpanToSentry(_span: Span): boolean { + return true; + } +} diff --git a/packages/node-experimental/src/sdk/trace.ts b/packages/opentelemetry/src/trace.ts similarity index 50% rename from packages/node-experimental/src/sdk/trace.ts rename to packages/opentelemetry/src/trace.ts index 72047f4478a3..e7a8fcaef8ea 100644 --- a/packages/node-experimental/src/sdk/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -1,14 +1,13 @@ -import type { Tracer } from '@opentelemetry/api'; -import { SpanStatusCode } from '@opentelemetry/api'; -import type { Span } from '@opentelemetry/sdk-trace-base'; -import { hasTracingEnabled } from '@sentry/core'; +import type { Span, Tracer } from '@opentelemetry/api'; +import { SpanStatusCode, trace } from '@opentelemetry/api'; +import { SDK_VERSION } from '@sentry/core'; +import type { Client } from '@sentry/types'; import { isThenable } from '@sentry/utils'; -import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SOURCE } from '../constants'; -import { setSpanMetadata } from '../opentelemetry/spanData'; -import type { NodeExperimentalClient, NodeExperimentalSpanContext } from '../types'; -import { spanIsSdkTraceBaseSpan } from '../utils/spanTypes'; -import { getCurrentHub } from './hub'; +import { getCurrentHub } from './custom/hub'; +import { InternalSentrySemanticAttributes } from './semanticAttributes'; +import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types'; +import { setSpanMetadata } from './utils/spanData'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -17,30 +16,18 @@ import { getCurrentHub } from './hub'; * * If you want to create a span that is not set as active, use {@link startInactiveSpan}. * - * Note that if you have not enabled tracing extensions via `addTracingExtensions` - * or you didn't set `tracesSampleRate`, this function will not generate spans - * and the `span` returned from the callback will be undefined. + * Note that you'll always get a span passed to the callback, it may just be a NonRecordingSpan if the span is not sampled. */ -export function startSpan(spanContext: NodeExperimentalSpanContext, callback: (span: Span | undefined) => T): T { +export function startSpan(spanContext: OpenTelemetrySpanContext, callback: (span: Span) => T): T { const tracer = getTracer(); - if (!tracer) { - return callback(undefined); - } const { name } = spanContext; - return tracer.startActiveSpan(name, (span): T => { + return tracer.startActiveSpan(name, span => { function finishSpan(): void { span.end(); } - // This is just a sanity check - in reality, this should not happen as we control the tracer, - // but to ensure type saftey we rather bail out here than to pass an invalid type out - if (!spanIsSdkTraceBaseSpan(span)) { - span.end(); - return callback(undefined); - } - _applySentryAttributesToSpan(span, spanContext); let maybePromiseResult: T; @@ -85,50 +72,36 @@ export const startActiveSpan = startSpan; * or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startInactiveSpan(spanContext: NodeExperimentalSpanContext): Span | undefined { +export function startInactiveSpan(spanContext: OpenTelemetrySpanContext): Span { const tracer = getTracer(); - if (!tracer) { - return undefined; - } const { name } = spanContext; const span = tracer.startSpan(name); - // This is just a sanity check - in reality, this should not happen as we control the tracer, - // but to ensure type saftey we rather bail out here than to pass an invalid type out - if (!spanIsSdkTraceBaseSpan(span)) { - span.end(); - return undefined; - } - _applySentryAttributesToSpan(span, spanContext); return span; } -function getTracer(): Tracer | undefined { - if (!hasTracingEnabled()) { - return undefined; - } - - const client = getCurrentHub().getClient(); - return client && client.tracer; +function getTracer(): Tracer { + const client = getCurrentHub().getClient(); + return (client && client.tracer) || trace.getTracer('@sentry/opentelemetry', SDK_VERSION); } -function _applySentryAttributesToSpan(span: Span, spanContext: NodeExperimentalSpanContext): void { +function _applySentryAttributesToSpan(span: Span, spanContext: OpenTelemetrySpanContext): void { const { origin, op, source, metadata } = spanContext; if (origin) { - span.setAttribute(OTEL_ATTR_ORIGIN, origin); + span.setAttribute(InternalSentrySemanticAttributes.ORIGIN, origin); } if (op) { - span.setAttribute(OTEL_ATTR_OP, op); + span.setAttribute(InternalSentrySemanticAttributes.OP, op); } if (source) { - span.setAttribute(OTEL_ATTR_SOURCE, source); + span.setAttribute(InternalSentrySemanticAttributes.SOURCE, source); } if (metadata) { diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts new file mode 100644 index 000000000000..0cb5342a3ac8 --- /dev/null +++ b/packages/opentelemetry/src/types.ts @@ -0,0 +1,29 @@ +import type { Span as WriteableSpan, Tracer } from '@opentelemetry/api'; +import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; +import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; + +export interface OpenTelemetryClient { + tracer: Tracer; + traceProvider: BasicTracerProvider | undefined; +} + +export interface OpenTelemetrySpanContext { + name: string; + op?: string; + metadata?: Partial; + origin?: SpanOrigin; + source?: TransactionSource; +} + +/** + * The base `Span` type is basically a `WriteableSpan`. + * There are places where we basically want to allow passing _any_ span, + * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. + * You'll have to make sure to check relevant fields before accessing them. + * + * Note that technically, the `Span` exported from `@opentelemwetry/sdk-trace-base` matches this, + * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. + */ +export type AbstractSpan = WriteableSpan | ReadableSpan; + +export type { Span }; diff --git a/packages/opentelemetry/src/utils/addOriginToSpan.ts b/packages/opentelemetry/src/utils/addOriginToSpan.ts new file mode 100644 index 000000000000..d91d35085603 --- /dev/null +++ b/packages/opentelemetry/src/utils/addOriginToSpan.ts @@ -0,0 +1,9 @@ +import type { Span } from '@opentelemetry/api'; +import type { SpanOrigin } from '@sentry/types'; + +import { InternalSentrySemanticAttributes } from '../semanticAttributes'; + +/** Adds an origin to an OTEL Span. */ +export function addOriginToSpan(span: Span, origin: SpanOrigin): void { + span.setAttribute(InternalSentrySemanticAttributes.ORIGIN, origin); +} diff --git a/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts b/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts new file mode 100644 index 000000000000..3dde27e49e9f --- /dev/null +++ b/packages/opentelemetry/src/utils/captureExceptionForTimedEvent.ts @@ -0,0 +1,55 @@ +import type { Span as OtelSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { Hub } from '@sentry/types'; +import { isString } from '@sentry/utils'; + +/** + * Maybe capture a Sentry exception for an OTEL timed event. + * This will check if the event is exception-like and in that case capture it as an exception. + */ +export function maybeCaptureExceptionForTimedEvent(hub: Hub, event: TimedEvent, otelSpan?: OtelSpan): void { + if (event.name !== 'exception') { + return; + } + + const attributes = event.attributes; + if (!attributes) { + return; + } + + const message = attributes[SemanticAttributes.EXCEPTION_MESSAGE]; + + if (typeof message !== 'string') { + return; + } + + const syntheticError = new Error(message); + + const stack = attributes[SemanticAttributes.EXCEPTION_STACKTRACE]; + if (isString(stack)) { + syntheticError.stack = stack; + } + + const type = attributes[SemanticAttributes.EXCEPTION_TYPE]; + if (isString(type)) { + syntheticError.name = type; + } + + hub.captureException(syntheticError, { + captureContext: otelSpan + ? { + contexts: { + otel: { + attributes: otelSpan.attributes, + resource: otelSpan.resource.attributes, + }, + trace: { + trace_id: otelSpan.spanContext().traceId, + span_id: otelSpan.spanContext().spanId, + parent_span_id: otelSpan.parentSpanId, + }, + }, + } + : undefined, + }); +} diff --git a/packages/opentelemetry/src/utils/contextData.ts b/packages/opentelemetry/src/utils/contextData.ts new file mode 100644 index 000000000000..899c55e3678d --- /dev/null +++ b/packages/opentelemetry/src/utils/contextData.ts @@ -0,0 +1,36 @@ +import type { Context } from '@opentelemetry/api'; +import type { Hub, PropagationContext } from '@sentry/types'; + +import { SENTRY_HUB_CONTEXT_KEY, SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY } from '../constants'; + +/** + * Try to get the Propagation Context from the given OTEL context. + * This requires the SentryPropagator to be registered. + */ +export function getPropagationContextFromContext(context: Context): PropagationContext | undefined { + return context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined; +} + +/** + * Set a Propagation Context on an OTEL context.. + * This will return a forked context with the Propagation Context set. + */ +export function setPropagationContextOnContext(context: Context, propagationContext: PropagationContext): Context { + return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); +} + +/** + * Try to get the Hub from the given OTEL context. + * This requires a Context Manager that was wrapped with getWrappedContextManager. + */ +export function getHubFromContext(context: Context): Hub | undefined { + return context.getValue(SENTRY_HUB_CONTEXT_KEY) as Hub | undefined; +} + +/** + * Set a Hub on an OTEL context.. + * This will return a forked context with the Propagation Context set. + */ +export function setHubOnContext(context: Context, hub: Hub): Context { + return context.setValue(SENTRY_HUB_CONTEXT_KEY, hub); +} diff --git a/packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts b/packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts similarity index 100% rename from packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts rename to packages/opentelemetry/src/utils/convertOtelTimeToSeconds.ts diff --git a/packages/node-experimental/src/utils/getActiveSpan.ts b/packages/opentelemetry/src/utils/getActiveSpan.ts similarity index 89% rename from packages/node-experimental/src/utils/getActiveSpan.ts rename to packages/opentelemetry/src/utils/getActiveSpan.ts index 240842770a68..1244a7cb4d62 100644 --- a/packages/node-experimental/src/utils/getActiveSpan.ts +++ b/packages/opentelemetry/src/utils/getActiveSpan.ts @@ -1,7 +1,7 @@ import type { Span } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import { getSpanParent } from '../opentelemetry/spanData'; +import { getSpanParent } from './spanData'; /** * Returns the currently active span. diff --git a/packages/node-experimental/src/utils/getRequestSpanData.ts b/packages/opentelemetry/src/utils/getRequestSpanData.ts similarity index 100% rename from packages/node-experimental/src/utils/getRequestSpanData.ts rename to packages/opentelemetry/src/utils/getRequestSpanData.ts diff --git a/packages/node-experimental/src/utils/getSpanKind.ts b/packages/opentelemetry/src/utils/getSpanKind.ts similarity index 80% rename from packages/node-experimental/src/utils/getSpanKind.ts rename to packages/opentelemetry/src/utils/getSpanKind.ts index 7769a1cd3290..72a7407049b9 100644 --- a/packages/node-experimental/src/utils/getSpanKind.ts +++ b/packages/opentelemetry/src/utils/getSpanKind.ts @@ -1,6 +1,6 @@ -import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; +import type { AbstractSpan } from '../types'; import { spanHasKind } from './spanTypes'; /** @@ -9,7 +9,7 @@ import { spanHasKind } from './spanTypes'; * so we need to check if we actually have a `SDKTraceBaseSpan` where we can fetch this from. * Otherwise, we fall back to `SpanKind.INTERNAL`. */ -export function getSpanKind(span: Span): SpanKind { +export function getSpanKind(span: AbstractSpan): SpanKind { if (spanHasKind(span)) { return span.kind; } diff --git a/packages/node-experimental/src/utils/groupSpansWithParents.ts b/packages/opentelemetry/src/utils/groupSpansWithParents.ts similarity index 97% rename from packages/node-experimental/src/utils/groupSpansWithParents.ts rename to packages/opentelemetry/src/utils/groupSpansWithParents.ts index 2af278d0bce2..6e5fb9e57c3a 100644 --- a/packages/node-experimental/src/utils/groupSpansWithParents.ts +++ b/packages/opentelemetry/src/utils/groupSpansWithParents.ts @@ -1,6 +1,6 @@ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { getSpanParent } from '../opentelemetry/spanData'; +import { getSpanParent } from './spanData'; export interface SpanNode { id: string; diff --git a/packages/opentelemetry/src/utils/isSentryRequest.ts b/packages/opentelemetry/src/utils/isSentryRequest.ts new file mode 100644 index 000000000000..361cc89d0ad7 --- /dev/null +++ b/packages/opentelemetry/src/utils/isSentryRequest.ts @@ -0,0 +1,26 @@ +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; + +import type { AbstractSpan } from '../types'; +import { spanHasAttributes } from './spanTypes'; + +/** + * + * @param otelSpan Checks wheter a given OTEL Span is an http request to sentry. + * @returns boolean + */ +export function isSentryRequestSpan(span: AbstractSpan): boolean { + if (!spanHasAttributes(span)) { + return false; + } + + const { attributes } = span; + + const httpUrl = attributes[SemanticAttributes.HTTP_URL]; + + if (!httpUrl) { + return false; + } + + return isSentryRequestUrl(httpUrl.toString(), getCurrentHub()); +} diff --git a/packages/opentelemetry/src/utils/mapStatus.ts b/packages/opentelemetry/src/utils/mapStatus.ts new file mode 100644 index 000000000000..065a626d1c38 --- /dev/null +++ b/packages/opentelemetry/src/utils/mapStatus.ts @@ -0,0 +1,74 @@ +import { SpanStatusCode } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { SpanStatusType as SentryStatus } from '@sentry/core'; + +import type { AbstractSpan } from '../types'; +import { spanHasAttributes, spanHasStatus } from './spanTypes'; + +// canonicalCodesHTTPMap maps some HTTP codes to Sentry's span statuses. See possible mapping in https://develop.sentry.dev/sdk/event-payloads/span/ +const canonicalCodesHTTPMap: Record = { + '400': 'failed_precondition', + '401': 'unauthenticated', + '403': 'permission_denied', + '404': 'not_found', + '409': 'aborted', + '429': 'resource_exhausted', + '499': 'cancelled', + '500': 'internal_error', + '501': 'unimplemented', + '503': 'unavailable', + '504': 'deadline_exceeded', +} as const; + +// canonicalCodesGrpcMap maps some GRPC codes to Sentry's span statuses. See description in grpc documentation. +const canonicalCodesGrpcMap: Record = { + '1': 'cancelled', + '2': 'unknown_error', + '3': 'invalid_argument', + '4': 'deadline_exceeded', + '5': 'not_found', + '6': 'already_exists', + '7': 'permission_denied', + '8': 'resource_exhausted', + '9': 'failed_precondition', + '10': 'aborted', + '11': 'out_of_range', + '12': 'unimplemented', + '13': 'internal_error', + '14': 'unavailable', + '15': 'data_loss', + '16': 'unauthenticated', +} as const; + +/** + * Get a Sentry span status from an otel span. + */ +export function mapStatus(span: AbstractSpan): SentryStatus { + const attributes = spanHasAttributes(span) ? span.attributes : {}; + const status = spanHasStatus(span) ? span.status : undefined; + + const httpCode = attributes[SemanticAttributes.HTTP_STATUS_CODE]; + const grpcCode = attributes[SemanticAttributes.RPC_GRPC_STATUS_CODE]; + + const code = typeof httpCode === 'string' ? httpCode : typeof httpCode === 'number' ? httpCode.toString() : undefined; + if (code) { + const sentryStatus = canonicalCodesHTTPMap[code]; + if (sentryStatus) { + return sentryStatus; + } + } + + if (typeof grpcCode === 'string') { + const sentryStatus = canonicalCodesGrpcMap[grpcCode]; + if (sentryStatus) { + return sentryStatus; + } + } + + const statusCode = status && status.code; + if (statusCode === SpanStatusCode.OK || statusCode === SpanStatusCode.UNSET) { + return 'ok'; + } + + return 'unknown_error'; +} diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts new file mode 100644 index 000000000000..784b268cc4f1 --- /dev/null +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -0,0 +1,166 @@ +import type { Attributes, AttributeValue } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { TransactionSource } from '@sentry/types'; +import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils'; + +import type { AbstractSpan } from '../types'; +import { getSpanKind } from './getSpanKind'; +import { spanHasAttributes, spanHasName } from './spanTypes'; + +interface SpanDescription { + op: string | undefined; + description: string; + source: TransactionSource; + data?: Record; +} + +/** + * Extract better op/description from an otel span. + * + * Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306 + */ +export function parseSpanDescription(span: AbstractSpan): SpanDescription { + const attributes = spanHasAttributes(span) ? span.attributes : {}; + const name = spanHasName(span) ? span.name : ''; + + // if http.method exists, this is an http request span + const httpMethod = attributes[SemanticAttributes.HTTP_METHOD]; + if (httpMethod) { + return descriptionForHttpMethod({ attributes, name, kind: getSpanKind(span) }, httpMethod); + } + + // If db.type exists then this is a database call span. + const dbSystem = attributes[SemanticAttributes.DB_SYSTEM]; + if (dbSystem) { + return descriptionForDbSystem({ attributes, name }); + } + + // If rpc.service exists then this is a rpc call span. + const rpcService = attributes[SemanticAttributes.RPC_SERVICE]; + if (rpcService) { + return { + op: 'rpc', + description: name, + source: 'route', + }; + } + + // If messaging.system exists then this is a messaging system span. + const messagingSystem = attributes[SemanticAttributes.MESSAGING_SYSTEM]; + if (messagingSystem) { + return { + op: 'message', + description: name, + source: 'route', + }; + } + + // If faas.trigger exists then this is a function as a service span. + const faasTrigger = attributes[SemanticAttributes.FAAS_TRIGGER]; + if (faasTrigger) { + return { op: faasTrigger.toString(), description: name, source: 'route' }; + } + + return { op: undefined, description: name, source: 'custom' }; +} + +function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription { + // Use DB statement (Ex "SELECT * FROM table") if possible as description. + const statement = attributes[SemanticAttributes.DB_STATEMENT]; + + const description = statement ? statement.toString() : name; + + return { op: 'db', description, source: 'task' }; +} + +/** Only exported for tests. */ +export function descriptionForHttpMethod( + { name, kind, attributes }: { name: string; attributes: Attributes; kind: SpanKind }, + httpMethod: AttributeValue, +): SpanDescription { + const opParts = ['http']; + + switch (kind) { + case SpanKind.CLIENT: + opParts.push('client'); + break; + case SpanKind.SERVER: + opParts.push('server'); + break; + } + + const { urlPath, url, query, fragment, hasRoute } = getSanitizedUrl(attributes, kind); + + if (!urlPath) { + return { op: opParts.join('.'), description: name, source: 'custom' }; + } + + // Ex. description="GET /api/users". + const description = `${httpMethod} ${urlPath}`; + + // If `httpPath` is a root path, then we can categorize the transaction source as route. + const source: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url'; + + const data: Record = {}; + + if (url) { + data.url = url; + } + if (query) { + data['http.query'] = query; + } + if (fragment) { + data['http.fragment'] = fragment; + } + + return { + op: opParts.join('.'), + description, + source, + data, + }; +} + +/** Exported for tests only */ +export function getSanitizedUrl( + attributes: Attributes, + kind: SpanKind, +): { + url: string | undefined; + urlPath: string | undefined; + query: string | undefined; + fragment: string | undefined; + hasRoute: boolean; +} { + // This is the relative path of the URL, e.g. /sub + const httpTarget = attributes[SemanticAttributes.HTTP_TARGET]; + // This is the full URL, including host & query params etc., e.g. https://example.com/sub?foo=bar + const httpUrl = attributes[SemanticAttributes.HTTP_URL]; + // This is the normalized route name - may not always be available! + const httpRoute = attributes[SemanticAttributes.HTTP_ROUTE]; + + const parsedUrl = typeof httpUrl === 'string' ? parseUrl(httpUrl) : undefined; + const url = parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined; + const query = parsedUrl && parsedUrl.search ? parsedUrl.search : undefined; + const fragment = parsedUrl && parsedUrl.hash ? parsedUrl.hash : undefined; + + if (typeof httpRoute === 'string') { + return { urlPath: httpRoute, url, query, fragment, hasRoute: true }; + } + + if (kind === SpanKind.SERVER && typeof httpTarget === 'string') { + return { urlPath: stripUrlQueryAndFragment(httpTarget), url, query, fragment, hasRoute: false }; + } + + if (parsedUrl) { + return { urlPath: url, url, query, fragment, hasRoute: false }; + } + + // fall back to target even for client spans, if no URL is present + if (typeof httpTarget === 'string') { + return { urlPath: stripUrlQueryAndFragment(httpTarget), url, query, fragment, hasRoute: false }; + } + + return { urlPath: undefined, url, query, fragment, hasRoute: false }; +} diff --git a/packages/node-experimental/src/opentelemetry/spanData.ts b/packages/opentelemetry/src/utils/spanData.ts similarity index 100% rename from packages/node-experimental/src/opentelemetry/spanData.ts rename to packages/opentelemetry/src/utils/spanData.ts diff --git a/packages/node-experimental/src/utils/spanTypes.ts b/packages/opentelemetry/src/utils/spanTypes.ts similarity index 68% rename from packages/node-experimental/src/utils/spanTypes.ts rename to packages/opentelemetry/src/utils/spanTypes.ts index 3883a97f8004..f92d411200a1 100644 --- a/packages/node-experimental/src/utils/spanTypes.ts +++ b/packages/opentelemetry/src/utils/spanTypes.ts @@ -1,6 +1,5 @@ -import type { SpanKind } from '@opentelemetry/api'; +import type { SpanKind, SpanStatus } from '@opentelemetry/api'; import type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; -import { Span as SdkTraceBaseSpan } from '@opentelemetry/sdk-trace-base'; import type { AbstractSpan } from '../types'; @@ -26,6 +25,28 @@ export function spanHasKind(span: SpanType): span return !!castSpan.kind; } +/** + * Check if a given span has a status. + * This is necessary because the base `Span` type does not have a status, + * so in places where we are passed a generic span, we need to check if we want to access it. + */ +export function spanHasStatus( + span: SpanType, +): span is SpanType & { status: SpanStatus } { + const castSpan = span as ReadableSpan; + return !!castSpan.status; +} + +/** + * Check if a given span has a name. + * This is necessary because the base `Span` type does not have a name, + * so in places where we are passed a generic span, we need to check if we want to access it. + */ +export function spanHasName(span: SpanType): span is SpanType & { name: string } { + const castSpan = span as ReadableSpan; + return !!castSpan.name; +} + /** * Check if a given span has a kind. * This is necessary because the base `Span` type does not have a kind, @@ -49,10 +70,3 @@ export function spanHasEvents( const castSpan = span as ReadableSpan; return Array.isArray(castSpan.events); } - -/** - * If the span is a SDK trace base span, which has some additional fields. - */ -export function spanIsSdkTraceBaseSpan(span: AbstractSpan): span is SdkTraceBaseSpan { - return span instanceof SdkTraceBaseSpan; -} diff --git a/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts b/packages/opentelemetry/test/asyncContextStrategy.test.ts similarity index 84% rename from packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts rename to packages/opentelemetry/test/asyncContextStrategy.test.ts index 518d61000fee..7f6039a03c0e 100644 --- a/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts +++ b/packages/opentelemetry/test/asyncContextStrategy.test.ts @@ -2,21 +2,20 @@ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Hub } from '@sentry/core'; import { runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { setupOtel } from '../../src/sdk/initOtel'; -import { setOtelContextAsyncContextStrategy } from '../../src/sdk/otelAsyncContextStrategy'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; -import { cleanupOtel } from '../helpers/mockSdkInit'; - -describe('otelAsyncContextStrategy', () => { +import { setOpenTelemetryContextAsyncContextStrategy } from '../src/asyncContextStrategy'; +import { getCurrentHub } from '../src/custom/hub'; +import { setupOtel } from './helpers/initOtel'; +import { cleanupOtel } from './helpers/mockSdkInit'; +import { getDefaultTestClientOptions, TestClient } from './helpers/TestClient'; + +describe('asyncContextStrategy', () => { let provider: BasicTracerProvider | undefined; beforeEach(() => { - const options = getDefaultNodeExperimentalClientOptions(); - const client = new NodeExperimentalClient(options); + const options = getDefaultTestClientOptions(); + const client = new TestClient(options); provider = setupOtel(client); - setOtelContextAsyncContextStrategy(); + setOpenTelemetryContextAsyncContextStrategy(); }); afterEach(() => { diff --git a/packages/opentelemetry/test/custom/client.test.ts b/packages/opentelemetry/test/custom/client.test.ts new file mode 100644 index 000000000000..d377522c9c21 --- /dev/null +++ b/packages/opentelemetry/test/custom/client.test.ts @@ -0,0 +1,19 @@ +import { ProxyTracer } from '@opentelemetry/api'; + +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; + +describe('OpenTelemetryClient', () => { + it('exposes a tracer', () => { + const options = getDefaultTestClientOptions(); + const client = new TestClient(options); + + const tracer = client.tracer; + expect(tracer).toBeDefined(); + expect(tracer).toBeInstanceOf(ProxyTracer); + + // Ensure we always get the same tracer instance + const tracer2 = client.tracer; + + expect(tracer2).toBe(tracer); + }); +}); diff --git a/packages/node-experimental/test/sdk/hub.test.ts b/packages/opentelemetry/test/custom/hub.test.ts similarity index 52% rename from packages/node-experimental/test/sdk/hub.test.ts rename to packages/opentelemetry/test/custom/hub.test.ts index a25de1565ad8..08e9b5e1bf90 100644 --- a/packages/node-experimental/test/sdk/hub.test.ts +++ b/packages/opentelemetry/test/custom/hub.test.ts @@ -1,43 +1,43 @@ -import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; +import { getCurrentHub, OpenTelemetryHub } from '../../src/custom/hub'; +import { OpenTelemetryScope } from '../../src/custom/scope'; -describe('NodeExperimentalHub', () => { +describe('OpenTelemetryHub', () => { it('getCurrentHub() returns the correct hub', () => { const hub = getCurrentHub(); expect(hub).toBeDefined(); - expect(hub).toBeInstanceOf(NodeExperimentalHub); + expect(hub).toBeInstanceOf(OpenTelemetryHub); const hub2 = getCurrentHub(); expect(hub2).toBe(hub); const scope = hub.getScope(); expect(scope).toBeDefined(); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); it('hub gets correct scope on initialization', () => { - const hub = new NodeExperimentalHub(); + const hub = new OpenTelemetryHub(); const scope = hub.getScope(); expect(scope).toBeDefined(); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); it('pushScope() creates correct scope', () => { - const hub = new NodeExperimentalHub(); + const hub = new OpenTelemetryHub(); const scope = hub.pushScope(); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); const scope2 = hub.getScope(); expect(scope2).toBe(scope); }); it('withScope() creates correct scope', () => { - const hub = new NodeExperimentalHub(); + const hub = new OpenTelemetryHub(); hub.withScope(scope => { - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); }); }); diff --git a/packages/node-experimental/test/sdk/hubextensions.test.ts b/packages/opentelemetry/test/custom/hubextensions.test.ts similarity index 52% rename from packages/node-experimental/test/sdk/hubextensions.test.ts rename to packages/opentelemetry/test/custom/hubextensions.test.ts index c2fee6baabde..47f6452062cc 100644 --- a/packages/node-experimental/test/sdk/hubextensions.test.ts +++ b/packages/opentelemetry/test/custom/hubextensions.test.ts @@ -1,7 +1,6 @@ -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { addTracingExtensions } from '../../src/sdk/hubextensions'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { getCurrentHub } from '../../src/custom/hub'; +import { addTracingExtensions } from '../../src/custom/hubextensions'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('hubextensions', () => { afterEach(() => { @@ -9,7 +8,7 @@ describe('hubextensions', () => { }); it('startTransaction is noop', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); getCurrentHub().bindClient(client); addTracingExtensions(); @@ -20,7 +19,7 @@ describe('hubextensions', () => { expect(mockConsole).toHaveBeenCalledTimes(1); expect(mockConsole).toHaveBeenCalledWith( - 'startTransaction is a noop in @sentry/node-experimental. Use `startSpan` instead.', + 'startTransaction is a noop in @sentry/opentelemetry. Use `startSpan` instead.', ); }); }); diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/opentelemetry/test/custom/scope.test.ts similarity index 81% rename from packages/node-experimental/test/sdk/scope.test.ts rename to packages/opentelemetry/test/custom/scope.test.ts index 7d8d772abd8c..6c96fab2f3c5 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/opentelemetry/test/custom/scope.test.ts @@ -1,15 +1,9 @@ import { makeSession } from '@sentry/core'; import type { Breadcrumb } from '@sentry/types'; -import { - OTEL_ATTR_BREADCRUMB_CATEGORY, - OTEL_ATTR_BREADCRUMB_DATA, - OTEL_ATTR_BREADCRUMB_EVENT_ID, - OTEL_ATTR_BREADCRUMB_LEVEL, - OTEL_ATTR_BREADCRUMB_TYPE, -} from '../../src/constants'; -import { setSpanParent } from '../../src/opentelemetry/spanData'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; +import { OpenTelemetryScope } from '../../src/custom/scope'; +import { InternalSentrySemanticAttributes } from '../../src/semanticAttributes'; +import { setSpanParent } from '../../src/utils/spanData'; import { createSpan } from '../helpers/createSpan'; import * as GetActiveSpan from './../../src/utils/getActiveSpan'; @@ -19,7 +13,7 @@ describe('NodeExperimentalScope', () => { }); it('clone() correctly clones the scope', () => { - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); scope['_breadcrumbs'] = [{ message: 'test' }]; scope['_tags'] = { tag: 'bar' }; @@ -36,9 +30,9 @@ describe('NodeExperimentalScope', () => { scope['_attachments'] = [{ data: '123', filename: 'test.txt' }]; scope['_sdkProcessingMetadata'] = { sdk: 'bar' }; - const scope2 = NodeExperimentalScope.clone(scope); + const scope2 = OpenTelemetryScope.clone(scope); - expect(scope2).toBeInstanceOf(NodeExperimentalScope); + expect(scope2).toBeInstanceOf(OpenTelemetryScope); expect(scope2).not.toBe(scope); // Ensure everything is correctly cloned @@ -74,13 +68,13 @@ describe('NodeExperimentalScope', () => { }); it('clone() works without existing scope', () => { - const scope = NodeExperimentalScope.clone(undefined); + const scope = OpenTelemetryScope.clone(undefined); - expect(scope).toBeInstanceOf(NodeExperimentalScope); + expect(scope).toBeInstanceOf(OpenTelemetryScope); }); it('getSpan returns undefined', () => { - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); // Pretend we have a _span set scope['_span'] = {} as any; @@ -89,7 +83,7 @@ describe('NodeExperimentalScope', () => { }); it('setSpan is a noop', () => { - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); scope.setSpan({} as any); @@ -100,7 +94,7 @@ describe('NodeExperimentalScope', () => { it('adds to scope if no root span is found', () => { jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { message: 'test' }; const now = Date.now(); @@ -115,7 +109,7 @@ describe('NodeExperimentalScope', () => { it('adds to scope if no root span is found & uses given timestamp', () => { jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { message: 'test', timestamp: 1234 }; scope.addBreadcrumb(breadcrumb); @@ -127,7 +121,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { message: 'test' }; const now = Date.now(); @@ -150,7 +144,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test' }; scope.addBreadcrumb(breadcrumb); @@ -169,7 +163,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb1: Breadcrumb = { timestamp: 12345, message: 'test1' }; const breadcrumb2: Breadcrumb = { timestamp: 5678, message: 'test2' }; const breadcrumb3: Breadcrumb = { timestamp: 9101112, message: 'test3' }; @@ -202,7 +196,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345 }; scope.addBreadcrumb(breadcrumb); @@ -221,7 +215,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test', @@ -240,11 +234,11 @@ describe('NodeExperimentalScope', () => { name: 'test', time: [12345, 0], attributes: { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), - [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', - [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', - [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', - [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [InternalSentrySemanticAttributes.BREADCRUMB_TYPE]: 'test-type', + [InternalSentrySemanticAttributes.BREADCRUMB_LEVEL]: 'info', + [InternalSentrySemanticAttributes.BREADCRUMB_EVENT_ID]: 'test-event-id', + [InternalSentrySemanticAttributes.BREADCRUMB_CATEGORY]: 'test-category', }, }), ]); @@ -254,7 +248,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test', data: {} }; scope.addBreadcrumb(breadcrumb); @@ -274,7 +268,7 @@ describe('NodeExperimentalScope', () => { it('gets from scope if no root span is found', () => { jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumbs: Breadcrumb[] = [ { message: 'test1', timestamp: 1234 }, { message: 'test2', timestamp: 12345 }, @@ -289,7 +283,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const now = Date.now(); @@ -298,18 +292,18 @@ describe('NodeExperimentalScope', () => { span.addEvent( 'breadcrumb event 2', { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), - [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', - [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', - [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', - [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [InternalSentrySemanticAttributes.BREADCRUMB_TYPE]: 'test-type', + [InternalSentrySemanticAttributes.BREADCRUMB_LEVEL]: 'info', + [InternalSentrySemanticAttributes.BREADCRUMB_EVENT_ID]: 'test-event-id', + [InternalSentrySemanticAttributes.BREADCRUMB_CATEGORY]: 'test-category', }, now + 3000, ); span.addEvent( 'breadcrumb event invalid JSON data', { - [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: 'this is not JSON...', }, now + 2000, ); @@ -339,7 +333,7 @@ describe('NodeExperimentalScope', () => { setSpanParent(span, parentSpan); setSpanParent(parentSpan, rootSpan); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const now = Date.now(); @@ -348,14 +342,14 @@ describe('NodeExperimentalScope', () => { span.addEvent( 'breadcrumb event 2', { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: true }), + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: true }), }, now + 3000, ); rootSpan.addEvent( 'breadcrumb event invalid JSON data', { - [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: 'this is not JSON...', }, now + 2000, ); @@ -372,7 +366,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const breadcrumbs: Breadcrumb[] = [ { message: 'test1', timestamp: 1234 }, @@ -399,7 +393,7 @@ describe('NodeExperimentalScope', () => { const span = createSpan(); jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); const now = Date.now(); @@ -408,18 +402,18 @@ describe('NodeExperimentalScope', () => { span.addEvent( 'breadcrumb event 2', { - [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), - [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', - [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', - [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', - [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [InternalSentrySemanticAttributes.BREADCRUMB_TYPE]: 'test-type', + [InternalSentrySemanticAttributes.BREADCRUMB_LEVEL]: 'info', + [InternalSentrySemanticAttributes.BREADCRUMB_EVENT_ID]: 'test-event-id', + [InternalSentrySemanticAttributes.BREADCRUMB_CATEGORY]: 'test-category', }, now + 3000, ); span.addEvent( 'breadcrumb event invalid JSON data', { - [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + [InternalSentrySemanticAttributes.BREADCRUMB_DATA]: 'this is not JSON...', }, now + 2000, ); diff --git a/packages/node-experimental/test/sdk/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts similarity index 77% rename from packages/node-experimental/test/sdk/transaction.test.ts rename to packages/opentelemetry/test/custom/transaction.test.ts index 132696655b09..65f5f79a87eb 100644 --- a/packages/node-experimental/test/sdk/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -1,8 +1,7 @@ -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { NodeExperimentalScope } from '../../src/sdk/scope'; -import { NodeExperimentalTransaction, startTransaction } from '../../src/sdk/transaction'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { getCurrentHub } from '../../src/custom/hub'; +import { OpenTelemetryScope } from '../../src/custom/scope'; +import { OpenTelemetryTransaction, startTransaction } from '../../src/custom/transaction'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('NodeExperimentalTransaction', () => { afterEach(() => { @@ -10,14 +9,14 @@ describe('NodeExperimentalTransaction', () => { }); it('works with finishWithScope without arguments', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new NodeExperimentalTransaction({ name: 'test' }, hub); + const transaction = new OpenTelemetryTransaction({ name: 'test' }, hub); transaction.sampled = true; const res = transaction.finishWithScope(); @@ -56,14 +55,14 @@ describe('NodeExperimentalTransaction', () => { }); it('works with finishWithScope with endTime', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new NodeExperimentalTransaction({ name: 'test', startTimestamp: 123456 }, hub); + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); transaction.sampled = true; const res = transaction.finishWithScope(1234567); @@ -81,17 +80,17 @@ describe('NodeExperimentalTransaction', () => { }); it('works with finishWithScope with endTime & scope', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new NodeExperimentalTransaction({ name: 'test', startTimestamp: 123456 }, hub); + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); transaction.sampled = true; - const scope = new NodeExperimentalScope(); + const scope = new OpenTelemetryScope(); scope.setTags({ tag1: 'yes', tag2: 'no', @@ -140,13 +139,13 @@ describe('startTranscation', () => { }); it('creates a NodeExperimentalTransaction', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 0 })); + const client = new TestClient(getDefaultTestClientOptions()); const hub = getCurrentHub(); hub.bindClient(client); const transaction = startTransaction(hub, { name: 'test' }); - expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); expect(transaction.sampled).toBe(undefined); expect(transaction.spanRecorder).toBeDefined(); @@ -167,7 +166,7 @@ describe('startTranscation', () => { }); it('allows to pass data to transaction', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const client = new TestClient(getDefaultTestClientOptions()); const hub = getCurrentHub(); hub.bindClient(client); @@ -178,7 +177,7 @@ describe('startTranscation', () => { traceId: 'trace1', }); - expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); expect(transaction.metadata).toEqual({ source: 'custom', diff --git a/packages/opentelemetry/test/helpers/TestClient.ts b/packages/opentelemetry/test/helpers/TestClient.ts new file mode 100644 index 000000000000..d2ed75175b8e --- /dev/null +++ b/packages/opentelemetry/test/helpers/TestClient.ts @@ -0,0 +1,49 @@ +import { BaseClient, createTransport, initAndBind } from '@sentry/core'; +import type { Client, ClientOptions, Event, Options, SeverityLevel } from '@sentry/types'; +import { resolvedSyncPromise } from '@sentry/utils'; + +import { wrapClientClass } from '../../src/custom/client'; +import type { OpenTelemetryClient } from '../../src/types'; + +class BaseTestClient extends BaseClient { + public constructor(options: ClientOptions) { + super(options); + } + + public eventFromException(exception: any): PromiseLike { + return resolvedSyncPromise({ + exception: { + values: [ + { + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + type: exception.name, + value: exception.message, + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + }, + ], + }, + }); + } + + public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { + return resolvedSyncPromise({ message, level }); + } +} + +export const TestClient = wrapClientClass(BaseTestClient); + +export type TestClientInterface = Client & OpenTelemetryClient; + +export function init(options: Partial = {}): void { + initAndBind(TestClient, getDefaultTestClientOptions(options)); +} + +export function getDefaultTestClientOptions(options: Partial = {}): ClientOptions { + return { + enableTracing: true, + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + } as ClientOptions; +} diff --git a/packages/opentelemetry/test/helpers/createSpan.ts b/packages/opentelemetry/test/helpers/createSpan.ts new file mode 100644 index 000000000000..38c4ed96f3a8 --- /dev/null +++ b/packages/opentelemetry/test/helpers/createSpan.ts @@ -0,0 +1,30 @@ +import type { Context, SpanContext } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import type { Tracer } from '@opentelemetry/sdk-trace-base'; +import { Span } from '@opentelemetry/sdk-trace-base'; +import { uuid4 } from '@sentry/utils'; + +export function createSpan( + name?: string, + { spanId, parentSpanId }: { spanId?: string; parentSpanId?: string } = {}, +): Span { + const spanProcessor = { + onStart: () => {}, + onEnd: () => {}, + }; + const tracer = { + resource: 'test-resource', + instrumentationLibrary: 'test-instrumentation-library', + getSpanLimits: () => ({}), + getActiveSpanProcessor: () => spanProcessor, + } as unknown as Tracer; + + const spanContext: SpanContext = { + spanId: spanId || uuid4(), + traceId: uuid4(), + traceFlags: 0, + }; + + // eslint-disable-next-line deprecation/deprecation + return new Span(tracer, {} as Context, name || 'test', spanContext, SpanKind.INTERNAL, parentSpanId); +} diff --git a/packages/opentelemetry/test/helpers/initOtel.ts b/packages/opentelemetry/test/helpers/initOtel.ts new file mode 100644 index 000000000000..91be948b2f9d --- /dev/null +++ b/packages/opentelemetry/test/helpers/initOtel.ts @@ -0,0 +1,72 @@ +import { diag, DiagLogLevel } from '@opentelemetry/api'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { Resource } from '@opentelemetry/resources'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +import { SDK_VERSION } from '@sentry/core'; +import { logger } from '@sentry/utils'; + +import { wrapContextManagerClass } from '../../src/contextManager'; +import { getCurrentHub } from '../../src/custom/hub'; +import { SentryPropagator } from '../../src/propagator'; +import { SentrySampler } from '../../src/sampler'; +import { setupEventContextTrace } from '../../src/setupEventContextTrace'; +import { SentrySpanProcessor } from '../../src/spanProcessor'; +import type { TestClientInterface } from './TestClient'; + +/** + * Initialize OpenTelemetry for Node. + */ +export function initOtel(): void { + const client = getCurrentHub().getClient(); + + if (!client) { + __DEBUG_BUILD__ && + logger.warn( + 'No client available, skipping OpenTelemetry setup. This probably means that `Sentry.init()` was not called before `initOtel()`.', + ); + return; + } + + if (client.getOptions().debug) { + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); + + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); + } + + setupEventContextTrace(client); + + const provider = setupOtel(client); + client.traceProvider = provider; +} + +/** Just exported for tests. */ +export function setupOtel(client: TestClientInterface): BasicTracerProvider { + // Create and configure NodeTracerProvider + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'opentelemetry-test', + [SemanticResourceAttributes.SERVICE_NAMESPACE]: 'sentry', + [SemanticResourceAttributes.SERVICE_VERSION]: SDK_VERSION, + }), + forceFlushTimeoutMillis: 500, + }); + provider.addSpanProcessor(new SentrySpanProcessor()); + + // We use a custom context manager to keep context in sync with sentry scope + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + + // Initialize the provider + provider.register({ + propagator: new SentryPropagator(), + contextManager: new SentryContextManager(), + }); + + return provider; +} diff --git a/packages/opentelemetry/test/helpers/mockSdkInit.ts b/packages/opentelemetry/test/helpers/mockSdkInit.ts new file mode 100644 index 000000000000..50dea300a4a9 --- /dev/null +++ b/packages/opentelemetry/test/helpers/mockSdkInit.ts @@ -0,0 +1,68 @@ +import { context, propagation, ProxyTracerProvider, trace } from '@opentelemetry/api'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { ClientOptions, Options } from '@sentry/types'; +import { GLOBAL_OBJ } from '@sentry/utils'; + +import { setOpenTelemetryContextAsyncContextStrategy } from '../../src/asyncContextStrategy'; +import { setupGlobalHub } from '../../src/custom/hub'; +import { initOtel } from './initOtel'; +import { init as initTestClient } from './TestClient'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +/** + * Initialize Sentry for Node. + */ +function init(options: Partial | undefined = {}): void { + setupGlobalHub(); + + const fullOptions: Partial = { + instrumenter: 'otel', + ...options, + }; + + initTestClient(fullOptions); + initOtel(); + setOpenTelemetryContextAsyncContextStrategy(); +} + +export function mockSdkInit(options?: Partial) { + GLOBAL_OBJ.__SENTRY__ = { + extensions: {}, + hub: undefined, + globalEventProcessors: [], + logger: undefined, + }; + + init({ dsn: PUBLIC_DSN, ...options }); +} + +export function cleanupOtel(_provider?: BasicTracerProvider): void { + const provider = getProvider(_provider); + + if (!provider) { + return; + } + + void provider.forceFlush(); + void provider.shutdown(); + + // Disable all globally registered APIs + trace.disable(); + context.disable(); + propagation.disable(); +} + +export function getProvider(_provider?: BasicTracerProvider): BasicTracerProvider | undefined { + let provider = _provider || trace.getTracerProvider(); + + if (provider instanceof ProxyTracerProvider) { + provider = provider.getDelegate(); + } + + if (!(provider instanceof BasicTracerProvider)) { + return undefined; + } + + return provider; +} diff --git a/packages/opentelemetry/test/integration/breadcrumbs.test.ts b/packages/opentelemetry/test/integration/breadcrumbs.test.ts new file mode 100644 index 000000000000..f56821a83e2a --- /dev/null +++ b/packages/opentelemetry/test/integration/breadcrumbs.test.ts @@ -0,0 +1,361 @@ +import { withScope } from '@sentry/core'; + +import { getCurrentHub, OpenTelemetryHub } from '../../src/custom/hub'; +import { startSpan } from '../../src/trace'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; + +describe('Integration | breadcrumbs', () => { + const beforeSendTransaction = jest.fn(() => null); + + afterEach(() => { + cleanupOtel(); + }); + + describe('without tracing', () => { + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + + const error = new Error('test'); + hub.captureException(error); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles parallel scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + + const error = new Error('test'); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test0' }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test2' }); + hub.captureException(error); + }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test3' }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test0', timestamp: 123456 }, + { message: 'test2', timestamp: 123456 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs for the current root span only', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); + + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + }); + }); + + startSpan({ name: 'test2' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test2-a', timestamp: 123456 }, + { message: 'test2-b', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('ignores scopes inside of root span', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(2); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles deep nesting of scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test3' }); + + startSpan({ name: 'inner3' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + + hub.captureException(error); + + startSpan({ name: 'inner4' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test5' }); + }); + + hub.addBreadcrumb({ timestamp: 123457, message: 'test6' }); + }); + }); + }); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test99' }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123457 }, + { message: 'test4', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs in async spans', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const error = new Error('test'); + + const promise1 = startSpan({ name: 'test' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + await startSpan({ name: 'inner1' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + await startSpan({ name: 'inner2' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + await new Promise(resolve => setTimeout(resolve, 10)); + + hub.captureException(error); + }); + + const promise2 = startSpan({ name: 'test-b' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); + + await startSpan({ name: 'inner1' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + await startSpan({ name: 'inner2' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + }); + }); + + await Promise.all([promise1, promise2]); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(6); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); +}); diff --git a/packages/node-experimental/test/integration/otelTimedEvents.test.ts b/packages/opentelemetry/test/integration/otelTimedEvents.test.ts similarity index 76% rename from packages/node-experimental/test/integration/otelTimedEvents.test.ts rename to packages/opentelemetry/test/integration/otelTimedEvents.test.ts index 8bdaec750a15..0fb1f1ff9d26 100644 --- a/packages/node-experimental/test/integration/otelTimedEvents.test.ts +++ b/packages/opentelemetry/test/integration/otelTimedEvents.test.ts @@ -1,9 +1,9 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import type { NodeExperimentalClient } from '../../src/sdk/client'; -import { getCurrentHub } from '../../src/sdk/hub'; -import { startSpan } from '../../src/sdk/trace'; +import { getCurrentHub } from '../../src/custom/hub'; +import { startSpan } from '../../src/trace'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; describe('Integration | OTEL TimedEvents', () => { afterEach(() => { @@ -17,15 +17,15 @@ describe('Integration | OTEL TimedEvents', () => { mockSdkInit({ beforeSend, beforeSendTransaction, enableTracing: true }); const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = hub.getClient() as TestClientInterface; startSpan({ name: 'test' }, span => { - span?.addEvent('exception', { + span.addEvent('exception', { [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', 'test-span-event-attr': 'test-span-event-attr-value', }); - span?.addEvent('other', { + span.addEvent('other', { [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message-2', 'test-span-event-attr': 'test-span-event-attr-value', }); @@ -39,8 +39,6 @@ describe('Integration | OTEL TimedEvents', () => { exception: { values: [ { - mechanism: { handled: true, type: 'generic' }, - stacktrace: expect.any(Object), type: 'Error', value: 'test-message', }, diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts new file mode 100644 index 000000000000..c028e1893d7a --- /dev/null +++ b/packages/opentelemetry/test/integration/scope.test.ts @@ -0,0 +1,238 @@ +import { captureException, setTag, withScope } from '@sentry/core'; + +import { getCurrentHub, OpenTelemetryHub } from '../../src/custom/hub'; +import { OpenTelemetryScope } from '../../src/custom/scope'; +import { startSpan } from '../../src/trace'; +import { getSpanScope } from '../../src/utils/spanData'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; + +describe('Integration | Scope', () => { + afterEach(() => { + cleanupOtel(); + }); + + describe.each([ + ['with tracing', true], + ['without tracing', false], + ])('%s', (_name, enableTracing) => { + it('correctly syncs OTEL context & Sentry hub/scope', async () => { + const beforeSend = jest.fn(() => null); + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const rootScope = hub.getScope(); + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + expect(rootScope).toBeInstanceOf(OpenTelemetryScope); + + const error = new Error('test error'); + let spanId: string | undefined; + let traceId: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + withScope(scope1 => { + scope1.setTag('tag2', 'val2'); + + withScope(scope2b => { + scope2b.setTag('tag3-b', 'val3-b'); + }); + + withScope(scope2 => { + scope2.setTag('tag3', 'val3'); + + startSpan({ name: 'outer' }, span => { + expect(getSpanScope(span)).toBe(enableTracing ? scope2 : undefined); + + spanId = span.spanContext().spanId; + traceId = span.spanContext().traceId; + + setTag('tag4', 'val4'); + + captureException(error); + }); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId + ? { + span_id: spanId, + trace_id: traceId, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + tag4: 'val4', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + + if (enableTracing) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + // Note: Scope for transaction is taken at `start` time, not `finish` time + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { 'otel.kind': 'INTERNAL' }, + span_id: spanId, + status: 'ok', + trace_id: traceId, + }, + }), + + spans: [], + start_timestamp: expect.any(Number), + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + timestamp: expect.any(Number), + transaction: 'outer', + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + } + }); + + it('isolates parallel root scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + const rootScope = hub.getScope(); + + expect(hub).toBeInstanceOf(OpenTelemetryHub); + expect(rootScope).toBeInstanceOf(OpenTelemetryScope); + + const error1 = new Error('test error 1'); + const error2 = new Error('test error 2'); + let spanId1: string | undefined; + let spanId2: string | undefined; + let traceId1: string | undefined; + let traceId2: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + withScope(scope1 => { + scope1.setTag('tag2', 'val2a'); + + withScope(scope2 => { + scope2.setTag('tag3', 'val3a'); + + startSpan({ name: 'outer' }, span => { + spanId1 = span.spanContext().spanId; + traceId1 = span.spanContext().traceId; + + setTag('tag4', 'val4a'); + + captureException(error1); + }); + }); + }); + + withScope(scope1 => { + scope1.setTag('tag2', 'val2b'); + + withScope(scope2 => { + scope2.setTag('tag3', 'val3b'); + + startSpan({ name: 'outer' }, span => { + spanId2 = span.spanContext().spanId; + traceId2 = span.spanContext().traceId; + + setTag('tag4', 'val4b'); + + captureException(error2); + }); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(2); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId1 + ? { + span_id: spanId1, + trace_id: traceId1, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2a', + tag3: 'val3a', + tag4: 'val4a', + }, + }), + { + event_id: expect.any(String), + originalException: error1, + syntheticException: expect.any(Error), + }, + ); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId2 + ? { + span_id: spanId2, + trace_id: traceId2, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2b', + tag3: 'val3b', + tag4: 'val4b', + }, + }), + { + event_id: expect.any(String), + originalException: error2, + syntheticException: expect.any(Error), + }, + ); + + if (enableTracing) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + } + }); + }); +}); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts new file mode 100644 index 000000000000..2a4de232cc1b --- /dev/null +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -0,0 +1,536 @@ +import { context, trace, TraceFlags } from '@opentelemetry/api'; +import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { addBreadcrumb, setTag } from '@sentry/core'; +import type { PropagationContext, TransactionEvent } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import { getCurrentHub } from '../../src/custom/hub'; +import { SentrySpanProcessor } from '../../src/spanProcessor'; +import { startInactiveSpan, startSpan } from '../../src/trace'; +import { setPropagationContextOnContext } from '../../src/utils/contextData'; +import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; + +describe('Integration | Transactions', () => { + afterEach(() => { + jest.restoreAllMocks(); + cleanupOtel(); + }); + + it('correctly creates transaction & spans', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + setTag('outer.tag', 'test value'); + + startSpan( + { + op: 'test op', + name: 'test name', + source: 'task', + origin: 'auto.test', + metadata: { requestPath: 'test-path' }, + }, + span => { + if (!span) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + setTag('test.tag', 'test value'); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }, + ); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(beforeSendTransaction).toHaveBeenLastCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: { + otel: { + attributes: { + 'test.outer': 'test value', + }, + resource: { + 'service.name': 'opentelemetry-test', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + environment: 'production', + event_id: expect.any(String), + sdkProcessingMetadata: expect.objectContaining({ + dynamicSamplingContext: expect.objectContaining({ + environment: 'production', + public_key: expect.any(String), + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + transaction: 'test name', + }), + propagationContext: { + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }, + sampleRate: 1, + source: 'task', + spanMetadata: expect.any(Object), + requestPath: 'test-path', + }), + // spans are circular (they have a reference to the transaction), which leads to jest choking on this + // instead we compare them in detail below + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + tags: { + 'outer.tag': 'test value', + }, + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + // Checking the spans here, as they are circular to the transaction... + const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; + const spans = runArgs[0].spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans.map(span => span.toJSON())).toEqual([ + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { 'otel.kind': 'INTERNAL', 'test.inner': 'test value' }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + ]); + }); + + it('correctly creates concurrent transaction & spans', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + + startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + if (!span) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + setTag('test.tag', 'test value'); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }); + + startSpan({ op: 'test op b', name: 'test name b' }, span => { + if (!span) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value b', + }); + + const subSpan = startInactiveSpan({ name: 'inner span 1b' }); + subSpan?.end(); + + setTag('test.tag', 'test value b'); + + startSpan({ name: 'inner span 2b' }, innerSpan => { + if (!innerSpan) { + return; + } + + addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value b', + }); + }); + }); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: { + 'test.outer': 'test value', + }, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }), + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + tags: {}, + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2b', timestamp: 123456 }, + { message: 'test breadcrumb 3b', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: { + 'test.outer': 'test value b', + }, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op b', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }), + spans: [ + expect.objectContaining({ + description: 'inner span 1b', + }), + expect.objectContaining({ + description: 'inner span 2b', + }), + ], + start_timestamp: expect.any(Number), + tags: {}, + timestamp: expect.any(Number), + transaction: 'test name b', + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + }); + + it('correctly creates transaction & spans with a trace header data', async () => { + const beforeSendTransaction = jest.fn(() => null); + + const traceId = 'd4cda95b652f4a1592b449d5929fda1b'; + const parentSpanId = '6e0c63257de34c92'; + + const spanContext = { + traceId, + spanId: parentSpanId, + sampled: true, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }; + + const propagationContext: PropagationContext = { + traceId, + parentSpanId, + spanId: '6e0c63257de34c93', + sampled: true, + }; + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), + () => { + startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + if (!span) { + return; + } + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + }); + }); + }, + ); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(beforeSendTransaction).toHaveBeenLastCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: {}, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + parent_span_id: parentSpanId, + status: 'ok', + trace_id: traceId, + }, + }), + // spans are circular (they have a reference to the transaction), which leads to jest choking on this + // instead we compare them in detail below + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + // Checking the spans here, as they are circular to the transaction... + const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; + const spans = runArgs[0].spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans.map(span => span.toJSON())).toEqual([ + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + }); + + it('cleans up spans that are not flushed for over 5 mins', async () => { + const beforeSendTransaction = jest.fn(() => null); + + const now = Date.now(); + jest.useFakeTimers(); + jest.setSystemTime(now); + + const logs: unknown[] = []; + jest.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as TestClientInterface; + const provider = getProvider(); + const multiSpanProcessor = provider?.activeSpanProcessor as + | (SpanProcessor & { _spanProcessors?: SpanProcessor[] }) + | undefined; + const spanProcessor = multiSpanProcessor?.['_spanProcessors']?.find( + spanProcessor => spanProcessor instanceof SentrySpanProcessor, + ) as SentrySpanProcessor | undefined; + + const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; + + if (!exporter) { + throw new Error('No exporter found, aborting test...'); + } + + let innerSpan1Id: string | undefined; + let innerSpan2Id: string | undefined; + + void startSpan({ name: 'test name' }, async span => { + if (!span) { + return; + } + + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + innerSpan1Id = subSpan?.spanContext().spanId; + subSpan?.end(); + + startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + innerSpan2Id = innerSpan.spanContext().spanId; + }); + + // Pretend this is pending for 10 minutes + await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); + }); + + // Nothing added to exporter yet + expect(exporter['_finishedSpans'].length).toBe(0); + + void client.flush(5_000); + jest.advanceTimersByTime(5_000); + + // Now the child-spans have been added to the exporter, but they are pending since they are waiting for their parant + expect(exporter['_finishedSpans'].length).toBe(2); + expect(beforeSendTransaction).toHaveBeenCalledTimes(0); + + // Now wait for 5 mins + jest.advanceTimersByTime(5 * 60 * 1_000); + + // Adding another span will trigger the cleanup + startSpan({ name: 'other span' }, () => {}); + + void client.flush(5_000); + jest.advanceTimersByTime(5_000); + + // Old spans have been cleared away + expect(exporter['_finishedSpans'].length).toBe(0); + + // Called once for the 'other span' + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + + expect(logs).toEqual( + expect.arrayContaining([ + 'SpanExporter exported 0 spans, 2 unsent spans remaining', + 'SpanExporter exported 1 spans, 2 unsent spans remaining', + `SpanExporter dropping span inner span 1 (${innerSpan1Id}) because it is pending for more than 5 minutes.`, + `SpanExporter dropping span inner span 2 (${innerSpan2Id}) because it is pending for more than 5 minutes.`, + ]), + ); + }); +}); diff --git a/packages/node-experimental/test/opentelemetry/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts similarity index 90% rename from packages/node-experimental/test/opentelemetry/propagator.test.ts rename to packages/opentelemetry/test/propagator.test.ts index 80b027496428..c90a2636a58a 100644 --- a/packages/node-experimental/test/opentelemetry/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -1,4 +1,3 @@ -import type { Context } from '@opentelemetry/api'; import { defaultTextMapGetter, defaultTextMapSetter, @@ -11,12 +10,9 @@ import { suppressTracing } from '@opentelemetry/core'; import { addTracingExtensions, Hub, makeMain } from '@sentry/core'; import type { PropagationContext } from '@sentry/types'; -import { - SENTRY_BAGGAGE_HEADER, - SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, - SENTRY_TRACE_HEADER, -} from '../../src/constants'; -import { SentryPropagator } from '../../src/opentelemetry/propagator'; +import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from '../src/constants'; +import { SentryPropagator } from '../src/propagator'; +import { getPropagationContextFromContext, setPropagationContextOnContext } from '../src/utils/contextData'; beforeAll(() => { addTracingExtensions(); @@ -167,7 +163,10 @@ describe('SentryPropagator', () => { 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c94-1', ], ])('%s', (_name, spanContext, propagationContext, baggage, sentryTrace) => { - const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + const context = trace.setSpanContext( + setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), + spanContext, + ); propagator.inject(context, carrier, defaultTextMapSetter); expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); @@ -194,7 +193,10 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.SAMPLED, }; - const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + const context = trace.setSpanContext( + setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), + spanContext, + ); const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( @@ -243,7 +245,7 @@ describe('SentryPropagator', () => { }, }; const context = suppressTracing( - trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext), + trace.setSpanContext(setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), spanContext), ); propagator.inject(context, carrier, defaultTextMapSetter); expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); @@ -267,7 +269,7 @@ describe('SentryPropagator', () => { }, }; - const context = setPropagationContext(ROOT_CONTEXT, propagationContext); + const context = setPropagationContextOnContext(ROOT_CONTEXT, propagationContext); propagator.inject(context, carrier, defaultTextMapSetter); expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( [ @@ -301,7 +303,7 @@ describe('SentryPropagator', () => { carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext; + const propagationContext = getPropagationContextFromContext(context); expect(propagationContext).toEqual({ sampled: true, parentSpanId: '6e0c63257de34c92', @@ -310,14 +312,14 @@ describe('SentryPropagator', () => { }); // Ensure spanId !== parentSpanId - it should be a new random ID - expect(propagationContext.spanId).not.toBe('6e0c63257de34c92'); + expect(propagationContext?.spanId).not.toBe('6e0c63257de34c92'); }); it('sets undefined sentry trace header on context', () => { const sentryTraceHeader = undefined; carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), @@ -329,7 +331,7 @@ describe('SentryPropagator', () => { 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), // Note: This is not automatically taken from the DSC (in reality, this should be aligned) @@ -347,7 +349,7 @@ describe('SentryPropagator', () => { const baggage = ''; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), @@ -357,7 +359,7 @@ describe('SentryPropagator', () => { it('handles when sentry-trace is an empty array', () => { carrier[SENTRY_TRACE_HEADER] = []; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + expect(getPropagationContextFromContext(context)).toEqual({ sampled: undefined, spanId: expect.any(String), traceId: expect.any(String), @@ -366,10 +368,6 @@ describe('SentryPropagator', () => { }); }); -function setPropagationContext(context: Context, propagationContext: PropagationContext): Context { - return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); -} - function baggageToArray(baggage: unknown): string[] { return typeof baggage === 'string' ? baggage.split(',').sort() : []; } diff --git a/packages/node-experimental/test/sdk/trace.test.ts b/packages/opentelemetry/test/trace.test.ts similarity index 52% rename from packages/node-experimental/test/sdk/trace.test.ts rename to packages/opentelemetry/test/trace.test.ts index e141372552a6..18037c7412af 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1,18 +1,17 @@ +import type { Span } from '@opentelemetry/api'; import { context, trace, TraceFlags } from '@opentelemetry/api'; -import type { Span } from '@opentelemetry/sdk-trace-base'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import type { PropagationContext } from '@sentry/types'; -import * as Sentry from '../../src'; -import { - OTEL_ATTR_OP, - OTEL_ATTR_ORIGIN, - OTEL_ATTR_SENTRY_SAMPLE_RATE, - OTEL_ATTR_SOURCE, - SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, -} from '../../src/constants'; -import { getSpanMetadata } from '../../src/opentelemetry/spanData'; -import { getActiveSpan } from '../../src/utils/getActiveSpan'; -import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import { getCurrentHub } from '../src/custom/hub'; +import { InternalSentrySemanticAttributes } from '../src/semanticAttributes'; +import { startInactiveSpan, startSpan } from '../src/trace'; +import type { AbstractSpan } from '../src/types'; +import { setPropagationContextOnContext } from '../src/utils/contextData'; +import { getActiveSpan, getRootSpan } from '../src/utils/getActiveSpan'; +import { getSpanMetadata } from '../src/utils/spanData'; +import { spanHasAttributes, spanHasName } from '../src/utils/spanTypes'; +import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; describe('trace', () => { beforeEach(() => { @@ -29,18 +28,18 @@ describe('trace', () => { expect(getActiveSpan()).toEqual(undefined); - const res = Sentry.startSpan({ name: 'outer' }, outerSpan => { + const res = startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - spans.push(outerSpan!); + spans.push(outerSpan); - expect(outerSpan?.name).toEqual('outer'); + expect(getSpanName(outerSpan)).toEqual('outer'); expect(getActiveSpan()).toEqual(outerSpan); - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - spans.push(innerSpan!); + spans.push(innerSpan); - expect(innerSpan?.name).toEqual('inner'); + expect(getSpanName(innerSpan)).toEqual('inner'); expect(getActiveSpan()).toEqual(innerSpan); }); @@ -53,11 +52,11 @@ describe('trace', () => { expect(spans).toHaveLength(2); const [outerSpan, innerSpan] = spans; - expect(outerSpan.name).toEqual('outer'); - expect(innerSpan.name).toEqual('inner'); + expect(getSpanName(outerSpan)).toEqual('outer'); + expect(getSpanName(innerSpan)).toEqual('inner'); - expect(outerSpan.endTime).not.toEqual([0, 0]); - expect(innerSpan.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(outerSpan)).not.toEqual([0, 0]); + expect(getSpanEndTime(innerSpan)).not.toEqual([0, 0]); }); it('works with an async callback', async () => { @@ -65,22 +64,22 @@ describe('trace', () => { expect(getActiveSpan()).toEqual(undefined); - const res = await Sentry.startSpan({ name: 'outer' }, async outerSpan => { + const res = await startSpan({ name: 'outer' }, async outerSpan => { expect(outerSpan).toBeDefined(); - spans.push(outerSpan!); + spans.push(outerSpan); await new Promise(resolve => setTimeout(resolve, 10)); - expect(outerSpan?.name).toEqual('outer'); + expect(getSpanName(outerSpan)).toEqual('outer'); expect(getActiveSpan()).toEqual(outerSpan); - await Sentry.startSpan({ name: 'inner' }, async innerSpan => { + await startSpan({ name: 'inner' }, async innerSpan => { expect(innerSpan).toBeDefined(); - spans.push(innerSpan!); + spans.push(innerSpan); await new Promise(resolve => setTimeout(resolve, 10)); - expect(innerSpan?.name).toEqual('inner'); + expect(getSpanName(innerSpan)).toEqual('inner'); expect(getActiveSpan()).toEqual(innerSpan); }); @@ -93,11 +92,11 @@ describe('trace', () => { expect(spans).toHaveLength(2); const [outerSpan, innerSpan] = spans; - expect(outerSpan.name).toEqual('outer'); - expect(innerSpan.name).toEqual('inner'); + expect(getSpanName(outerSpan)).toEqual('outer'); + expect(getSpanName(innerSpan)).toEqual('inner'); - expect(outerSpan.endTime).not.toEqual([0, 0]); - expect(innerSpan.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(outerSpan)).not.toEqual([0, 0]); + expect(getSpanEndTime(innerSpan)).not.toEqual([0, 0]); }); it('works with multiple parallel calls', () => { @@ -106,34 +105,34 @@ describe('trace', () => { expect(getActiveSpan()).toEqual(undefined); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - spans1.push(outerSpan!); + spans1.push(outerSpan); - expect(outerSpan?.name).toEqual('outer'); + expect(getSpanName(outerSpan)).toEqual('outer'); expect(getActiveSpan()).toEqual(outerSpan); - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - spans1.push(innerSpan!); + spans1.push(innerSpan); - expect(innerSpan?.name).toEqual('inner'); + expect(getSpanName(innerSpan)).toEqual('inner'); expect(getActiveSpan()).toEqual(innerSpan); }); }); - Sentry.startSpan({ name: 'outer2' }, outerSpan => { + startSpan({ name: 'outer2' }, outerSpan => { expect(outerSpan).toBeDefined(); - spans2.push(outerSpan!); + spans2.push(outerSpan); - expect(outerSpan?.name).toEqual('outer2'); + expect(getSpanName(outerSpan)).toEqual('outer2'); expect(getActiveSpan()).toEqual(outerSpan); - Sentry.startSpan({ name: 'inner2' }, innerSpan => { + startSpan({ name: 'inner2' }, innerSpan => { expect(innerSpan).toBeDefined(); - spans2.push(innerSpan!); + spans2.push(innerSpan); - expect(innerSpan?.name).toEqual('inner2'); + expect(getSpanName(innerSpan)).toEqual('inner2'); expect(getActiveSpan()).toEqual(innerSpan); }); }); @@ -143,22 +142,75 @@ describe('trace', () => { expect(spans2).toHaveLength(2); }); + it('works with multiple parallel async calls', async () => { + const spans1: Span[] = []; + const spans2: Span[] = []; + + expect(getActiveSpan()).toEqual(undefined); + + const promise1 = startSpan({ name: 'outer' }, async outerSpan => { + expect(outerSpan).toBeDefined(); + spans1.push(outerSpan); + + expect(getSpanName(outerSpan)).toEqual('outer'); + expect(getActiveSpan()).toEqual(outerSpan); + expect(getRootSpan(outerSpan)).toEqual(outerSpan); + + await new Promise(resolve => setTimeout(resolve, 10)); + + await startSpan({ name: 'inner' }, async innerSpan => { + expect(innerSpan).toBeDefined(); + spans1.push(innerSpan); + + expect(getSpanName(innerSpan)).toEqual('inner'); + expect(getActiveSpan()).toEqual(innerSpan); + expect(getRootSpan(innerSpan)).toEqual(outerSpan); + }); + }); + + const promise2 = startSpan({ name: 'outer2' }, async outerSpan => { + expect(outerSpan).toBeDefined(); + spans2.push(outerSpan); + + expect(getSpanName(outerSpan)).toEqual('outer2'); + expect(getActiveSpan()).toEqual(outerSpan); + expect(getRootSpan(outerSpan)).toEqual(outerSpan); + + await new Promise(resolve => setTimeout(resolve, 10)); + + await startSpan({ name: 'inner2' }, async innerSpan => { + expect(innerSpan).toBeDefined(); + spans2.push(innerSpan); + + expect(getSpanName(innerSpan)).toEqual('inner2'); + expect(getActiveSpan()).toEqual(innerSpan); + expect(getRootSpan(innerSpan)).toEqual(outerSpan); + }); + }); + + await Promise.all([promise1, promise2]); + + expect(getActiveSpan()).toEqual(undefined); + expect(spans1).toHaveLength(2); + expect(spans2).toHaveLength(2); + }); + it('allows to pass context arguments', () => { - Sentry.startSpan( + startSpan( { name: 'outer', }, span => { expect(span).toBeDefined(); - expect(span?.attributes).toEqual({ - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + expect(getSpanAttributes(span)).toEqual({ + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, }); - expect(getSpanMetadata(span!)).toEqual(undefined); + expect(getSpanMetadata(span)).toEqual(undefined); }, ); - Sentry.startSpan( + startSpan( { name: 'outer', op: 'my-op', @@ -168,14 +220,14 @@ describe('trace', () => { }, span => { expect(span).toBeDefined(); - expect(span?.attributes).toEqual({ - [OTEL_ATTR_SOURCE]: 'task', - [OTEL_ATTR_ORIGIN]: 'auto.test.origin', - [OTEL_ATTR_OP]: 'my-op', - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + expect(getSpanAttributes(span)).toEqual({ + [InternalSentrySemanticAttributes.SOURCE]: 'task', + [InternalSentrySemanticAttributes.ORIGIN]: 'auto.test.origin', + [InternalSentrySemanticAttributes.OP]: 'my-op', + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, }); - expect(getSpanMetadata(span!)).toEqual({ requestPath: 'test-path' }); + expect(getSpanMetadata(span)).toEqual({ requestPath: 'test-path' }); }, ); }); @@ -183,51 +235,51 @@ describe('trace', () => { describe('startInactiveSpan', () => { it('works at the root', () => { - const span = Sentry.startInactiveSpan({ name: 'test' }); + const span = startInactiveSpan({ name: 'test' }); expect(span).toBeDefined(); - expect(span?.name).toEqual('test'); - expect(span?.endTime).toEqual([0, 0]); + expect(getSpanName(span)).toEqual('test'); + expect(getSpanEndTime(span)).toEqual([0, 0]); expect(getActiveSpan()).toBeUndefined(); - span?.end(); + span.end(); - expect(span?.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(span)).not.toEqual([0, 0]); expect(getActiveSpan()).toBeUndefined(); }); it('works as a child span', () => { - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); expect(getActiveSpan()).toEqual(outerSpan); - const innerSpan = Sentry.startInactiveSpan({ name: 'test' }); + const innerSpan = startInactiveSpan({ name: 'test' }); expect(innerSpan).toBeDefined(); - expect(innerSpan?.name).toEqual('test'); - expect(innerSpan?.endTime).toEqual([0, 0]); + expect(getSpanName(innerSpan)).toEqual('test'); + expect(getSpanEndTime(innerSpan)).toEqual([0, 0]); expect(getActiveSpan()).toEqual(outerSpan); - innerSpan?.end(); + innerSpan.end(); - expect(innerSpan?.endTime).not.toEqual([0, 0]); + expect(getSpanEndTime(innerSpan)).not.toEqual([0, 0]); expect(getActiveSpan()).toEqual(outerSpan); }); }); it('allows to pass context arguments', () => { - const span = Sentry.startInactiveSpan({ + const span = startInactiveSpan({ name: 'outer', }); expect(span).toBeDefined(); - expect(span?.attributes).toEqual({ - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + expect(getSpanAttributes(span)).toEqual({ + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, }); - expect(getSpanMetadata(span!)).toEqual(undefined); + expect(getSpanMetadata(span)).toEqual(undefined); - const span2 = Sentry.startInactiveSpan({ + const span2 = startInactiveSpan({ name: 'outer', op: 'my-op', origin: 'auto.test.origin', @@ -236,14 +288,14 @@ describe('trace', () => { }); expect(span2).toBeDefined(); - expect(span2?.attributes).toEqual({ - [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, - [OTEL_ATTR_SOURCE]: 'task', - [OTEL_ATTR_ORIGIN]: 'auto.test.origin', - [OTEL_ATTR_OP]: 'my-op', + expect(getSpanAttributes(span2)).toEqual({ + [InternalSentrySemanticAttributes.SAMPLE_RATE]: 1, + [InternalSentrySemanticAttributes.SOURCE]: 'task', + [InternalSentrySemanticAttributes.ORIGIN]: 'auto.test.origin', + [InternalSentrySemanticAttributes.OP]: 'my-op', }); - expect(getSpanMetadata(span2!)).toEqual({ requestPath: 'test-path' }); + expect(getSpanMetadata(span2)).toEqual({ requestPath: 'test-path' }); }); }); }); @@ -258,8 +310,9 @@ describe('trace (tracing disabled)', () => { }); it('startSpan calls callback without span', () => { - const val = Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + const val = startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); return 'test value'; }); @@ -267,10 +320,11 @@ describe('trace (tracing disabled)', () => { expect(val).toEqual('test value'); }); - it('startInactiveSpan returns undefined', () => { - const span = Sentry.startInactiveSpan({ name: 'test' }); + it('startInactiveSpan returns a NonRecordinSpan', () => { + const span = startInactiveSpan({ name: 'test' }); - expect(span).toBeUndefined(); + expect(span).toBeDefined(); + expect(span.isRecording()).toBe(false); }); }); @@ -285,11 +339,13 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 0.5 }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); - Sentry.startSpan({ name: 'inner' }, innerSpan => { - expect(innerSpan).toBeUndefined(); + startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + expect(innerSpan.isRecording()).toBe(false); }); }); }); @@ -299,16 +355,16 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 0.5 }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - expect(outerSpan?.isRecording()).toBe(true); + expect(outerSpan.isRecording()).toBe(true); // All fields are empty for NonRecordingSpan - expect(outerSpan?.name).toBe('outer'); + expect(getSpanName(outerSpan)).toBe('outer'); - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - expect(innerSpan?.isRecording()).toBe(true); - expect(innerSpan?.name).toBe('inner'); + expect(innerSpan.isRecording()).toBe(true); + expect(getSpanName(innerSpan)).toBe('inner'); }); }); }); @@ -319,20 +375,20 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 1 }); // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - expect(outerSpan?.isRecording()).toBe(true); - expect(outerSpan?.name).toBe('outer'); + expect(outerSpan.isRecording()).toBe(true); + expect(getSpanName(outerSpan)).toBe('outer'); // Now let's mutate the tracesSampleRate so that the next entry _should_ not be sampled // but it will because of parent sampling - const client = Sentry.getCurrentHub().getClient(); + const client = getCurrentHub().getClient(); client!.getOptions().tracesSampleRate = 0.5; - Sentry.startSpan({ name: 'inner' }, innerSpan => { + startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); - expect(innerSpan?.isRecording()).toBe(true); - expect(innerSpan?.name).toBe('inner'); + expect(innerSpan.isRecording()).toBe(true); + expect(getSpanName(innerSpan)).toBe('inner'); }); }); }); @@ -342,17 +398,19 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampleRate: 0.5 }); - // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + // This will def. be unsampled because of the tracesSampleRate + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); - // Now let's mutate the tracesSampleRate so that the next entry _should_ not be sampled - // but it will because of parent sampling - const client = Sentry.getCurrentHub().getClient(); + // Now let's mutate the tracesSampleRate so that the next entry _should_ be sampled + // but it will remain unsampled because of parent sampling + const client = getCurrentHub().getClient(); client!.getOptions().tracesSampleRate = 1; - Sentry.startSpan({ name: 'inner' }, innerSpan => { - expect(innerSpan).toBeUndefined(); + startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + expect(innerSpan.isRecording()).toBe(false); }); }); }); @@ -382,16 +440,13 @@ describe('trace (sampling)', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - expect(outerSpan?.isRecording()).toBe(true); - expect(outerSpan?.name).toBe('outer'); + expect(outerSpan.isRecording()).toBe(true); + expect(getSpanName(outerSpan)).toBe('outer'); }); }, ); @@ -422,14 +477,12 @@ describe('trace (sampling)', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); }); }, ); @@ -444,7 +497,7 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampler }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); }); @@ -457,11 +510,11 @@ describe('trace (sampling)', () => { // Now return `false`, it should not sample tracesSamplerResponse = false; - Sentry.startSpan({ name: 'outer2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer2' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); - Sentry.startSpan({ name: 'inner2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'inner2' }, innerSpan => { + expect(innerSpan.isRecording()).toBe(false); }); }); @@ -483,7 +536,7 @@ describe('trace (sampling)', () => { mockSdkInit({ tracesSampler }); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); }); @@ -496,11 +549,11 @@ describe('trace (sampling)', () => { // Now return `0`, it should not sample tracesSamplerResponse = 0; - Sentry.startSpan({ name: 'outer2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer2' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); - Sentry.startSpan({ name: 'inner2' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'inner2' }, innerSpan => { + expect(innerSpan.isRecording()).toBe(false); }); }); @@ -513,8 +566,8 @@ describe('trace (sampling)', () => { // Now return `0.4`, it should not sample tracesSamplerResponse = 0.4; - Sentry.startSpan({ name: 'outer3' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer3' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); }); expect(tracesSampler).toHaveBeenCalledTimes(4); @@ -550,14 +603,11 @@ describe('trace (sampling)', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( - trace.setSpanContext( - context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), - spanContext, - ), + trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), () => { // This will def. be sampled because of the tracesSampleRate - Sentry.startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeUndefined(); + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); }); }, ); @@ -572,3 +622,15 @@ describe('trace (sampling)', () => { }); }); }); + +function getSpanName(span: AbstractSpan): string | undefined { + return spanHasName(span) ? span.name : undefined; +} + +function getSpanEndTime(span: AbstractSpan): [number, number] | undefined { + return (span as ReadableSpan).endTime; +} + +function getSpanAttributes(span: AbstractSpan): Record | undefined { + return spanHasAttributes(span) ? span.attributes : undefined; +} diff --git a/packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts b/packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts new file mode 100644 index 000000000000..4d0c39b3a8b9 --- /dev/null +++ b/packages/opentelemetry/test/utils/captureExceptionForTimedEvent.test.ts @@ -0,0 +1,147 @@ +import type { Span as OtelSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { Hub } from '@sentry/types'; + +import { maybeCaptureExceptionForTimedEvent } from '../../src/utils/captureExceptionForTimedEvent'; + +describe('maybeCaptureExceptionForTimedEvent', () => { + it('ignores non-exception events', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'test event', + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).not.toHaveBeenCalled(); + }); + + it('ignores exception events without EXCEPTION_MESSAGE', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).not.toHaveBeenCalled(); + }); + + it('captures exception from event with EXCEPTION_MESSAGE', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + attributes: { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', + }, + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(expect.objectContaining({ message: 'test-message' }), { + captureContext: undefined, + }); + expect(captureException).toHaveBeenCalledWith(expect.any(Error), { + captureContext: undefined, + }); + }); + + it('captures stack and type, if available', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + attributes: { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', + [SemanticAttributes.EXCEPTION_STACKTRACE]: 'test-stack', + [SemanticAttributes.EXCEPTION_TYPE]: 'test-type', + }, + }; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith( + expect.objectContaining({ message: 'test-message', name: 'test-type', stack: 'test-stack' }), + { + captureContext: undefined, + }, + ); + expect(captureException).toHaveBeenCalledWith(expect.any(Error), { + captureContext: undefined, + }); + }); + + it('captures span context, if available', async () => { + const event: TimedEvent = { + time: [12345, 0], + name: 'exception', + attributes: { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', + }, + }; + + const span = { + parentSpanId: 'test-parent-span-id', + attributes: { + 'test-attr1': 'test-value1', + }, + resource: { + attributes: { + 'test-attr2': 'test-value2', + }, + }, + spanContext: () => { + return { spanId: 'test-span-id', traceId: 'test-trace-id' }; + }, + } as unknown as OtelSpan; + + const captureException = jest.fn(); + const hub = { + captureException, + } as unknown as Hub; + + maybeCaptureExceptionForTimedEvent(hub, event, span); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(expect.objectContaining({ message: 'test-message' }), { + captureContext: { + contexts: { + otel: { + attributes: { + 'test-attr1': 'test-value1', + }, + resource: { + 'test-attr2': 'test-value2', + }, + }, + trace: { + trace_id: 'test-trace-id', + span_id: 'test-span-id', + parent_span_id: 'test-parent-span-id', + }, + }, + }, + }); + }); +}); diff --git a/packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts b/packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts similarity index 100% rename from packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts rename to packages/opentelemetry/test/utils/convertOtelTimeToSeconds.test.ts diff --git a/packages/node-experimental/test/utils/getActiveSpan.test.ts b/packages/opentelemetry/test/utils/getActiveSpan.test.ts similarity index 88% rename from packages/node-experimental/test/utils/getActiveSpan.test.ts rename to packages/opentelemetry/test/utils/getActiveSpan.test.ts index b97ced5bdbf8..b3a2f359bfbd 100644 --- a/packages/node-experimental/test/utils/getActiveSpan.test.ts +++ b/packages/opentelemetry/test/utils/getActiveSpan.test.ts @@ -1,18 +1,16 @@ import { trace } from '@opentelemetry/api'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { setupOtel } from '../../src/sdk/initOtel'; import { getActiveSpan, getRootSpan } from '../../src/utils/getActiveSpan'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('getActiveSpan', () => { let provider: BasicTracerProvider | undefined; beforeEach(() => { - const options = getDefaultNodeExperimentalClientOptions(); - const client = new NodeExperimentalClient(options); + const client = new TestClient(getDefaultTestClientOptions()); provider = setupOtel(client); }); @@ -97,8 +95,7 @@ describe('getRootSpan', () => { let provider: BasicTracerProvider | undefined; beforeEach(() => { - const options = getDefaultNodeExperimentalClientOptions(); - const client = new NodeExperimentalClient(options); + const client = new TestClient(getDefaultTestClientOptions()); provider = setupOtel(client); }); diff --git a/packages/node-experimental/test/utils/getRequestSpanData.test.ts b/packages/opentelemetry/test/utils/getRequestSpanData.test.ts similarity index 100% rename from packages/node-experimental/test/utils/getRequestSpanData.test.ts rename to packages/opentelemetry/test/utils/getRequestSpanData.test.ts diff --git a/packages/opentelemetry/test/utils/getSpanKind.test.ts b/packages/opentelemetry/test/utils/getSpanKind.test.ts new file mode 100644 index 000000000000..50e57ee4fac7 --- /dev/null +++ b/packages/opentelemetry/test/utils/getSpanKind.test.ts @@ -0,0 +1,11 @@ +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; + +import { getSpanKind } from '../../src/utils/getSpanKind'; + +describe('getSpanKind', () => { + it('works', () => { + expect(getSpanKind({} as Span)).toBe(SpanKind.INTERNAL); + expect(getSpanKind({ kind: SpanKind.CLIENT } as unknown as Span)).toBe(SpanKind.CLIENT); + }); +}); diff --git a/packages/node-experimental/test/utils/groupSpansWithParents.test.ts b/packages/opentelemetry/test/utils/groupSpansWithParents.test.ts similarity index 100% rename from packages/node-experimental/test/utils/groupSpansWithParents.test.ts rename to packages/opentelemetry/test/utils/groupSpansWithParents.test.ts diff --git a/packages/opentelemetry/test/utils/mapStatus.test.ts b/packages/opentelemetry/test/utils/mapStatus.test.ts new file mode 100644 index 000000000000..4fa6cc664b61 --- /dev/null +++ b/packages/opentelemetry/test/utils/mapStatus.test.ts @@ -0,0 +1,83 @@ +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { SpanStatusType } from '@sentry/core'; + +import { mapStatus } from '../../src/utils/mapStatus'; +import { createSpan } from '../helpers/createSpan'; + +describe('mapStatus', () => { + const statusTestTable: [number, undefined | number | string, undefined | string, SpanStatusType][] = [ + [-1, undefined, undefined, 'unknown_error'], + [3, undefined, undefined, 'unknown_error'], + [0, undefined, undefined, 'ok'], + [1, undefined, undefined, 'ok'], + [2, undefined, undefined, 'unknown_error'], + + // http codes + [2, 400, undefined, 'failed_precondition'], + [2, 401, undefined, 'unauthenticated'], + [2, 403, undefined, 'permission_denied'], + [2, 404, undefined, 'not_found'], + [2, 409, undefined, 'aborted'], + [2, 429, undefined, 'resource_exhausted'], + [2, 499, undefined, 'cancelled'], + [2, 500, undefined, 'internal_error'], + [2, 501, undefined, 'unimplemented'], + [2, 503, undefined, 'unavailable'], + [2, 504, undefined, 'deadline_exceeded'], + [2, 999, undefined, 'unknown_error'], + + [2, '400', undefined, 'failed_precondition'], + [2, '401', undefined, 'unauthenticated'], + [2, '403', undefined, 'permission_denied'], + [2, '404', undefined, 'not_found'], + [2, '409', undefined, 'aborted'], + [2, '429', undefined, 'resource_exhausted'], + [2, '499', undefined, 'cancelled'], + [2, '500', undefined, 'internal_error'], + [2, '501', undefined, 'unimplemented'], + [2, '503', undefined, 'unavailable'], + [2, '504', undefined, 'deadline_exceeded'], + [2, '999', undefined, 'unknown_error'], + + // grpc codes + [2, undefined, '1', 'cancelled'], + [2, undefined, '2', 'unknown_error'], + [2, undefined, '3', 'invalid_argument'], + [2, undefined, '4', 'deadline_exceeded'], + [2, undefined, '5', 'not_found'], + [2, undefined, '6', 'already_exists'], + [2, undefined, '7', 'permission_denied'], + [2, undefined, '8', 'resource_exhausted'], + [2, undefined, '9', 'failed_precondition'], + [2, undefined, '10', 'aborted'], + [2, undefined, '11', 'out_of_range'], + [2, undefined, '12', 'unimplemented'], + [2, undefined, '13', 'internal_error'], + [2, undefined, '14', 'unavailable'], + [2, undefined, '15', 'data_loss'], + [2, undefined, '16', 'unauthenticated'], + [2, undefined, '999', 'unknown_error'], + + // http takes precedence over grpc + [2, '400', '2', 'failed_precondition'], + ]; + + it.each(statusTestTable)( + 'works with otelStatus=%i, httpCode=%s, grpcCode=%s', + (otelStatus, httpCode, grpcCode, expected) => { + const span = createSpan(); + span.setStatus({ code: otelStatus }); + + if (httpCode) { + span.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, httpCode); + } + + if (grpcCode) { + span.setAttribute(SemanticAttributes.RPC_GRPC_STATUS_CODE, grpcCode); + } + + const actual = mapStatus(span); + expect(actual).toEqual(expected); + }, + ); +}); diff --git a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts new file mode 100644 index 000000000000..aa78526f8ffe --- /dev/null +++ b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts @@ -0,0 +1,340 @@ +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +import { descriptionForHttpMethod, getSanitizedUrl, parseSpanDescription } from '../../src/utils/parseSpanDescription'; + +describe('parseSpanDescription', () => { + it.each([ + [ + 'works without attributes & name', + undefined, + undefined, + undefined, + { + description: '', + op: undefined, + source: 'custom', + }, + ], + [ + 'works with empty attributes', + {}, + 'test name', + SpanKind.CLIENT, + { + description: 'test name', + op: undefined, + source: 'custom', + }, + ], + [ + 'works with http method', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'test name', + op: 'http.client', + source: 'custom', + }, + ], + [ + 'works with db system', + { + [SemanticAttributes.DB_SYSTEM]: 'mysql', + [SemanticAttributes.DB_STATEMENT]: 'SELECT * from users', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'SELECT * from users', + op: 'db', + source: 'task', + }, + ], + [ + 'works with db system without statement', + { + [SemanticAttributes.DB_SYSTEM]: 'mysql', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'test name', + op: 'db', + source: 'task', + }, + ], + [ + 'works with rpc service', + { + [SemanticAttributes.RPC_SERVICE]: 'rpc-test-service', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'rpc', + source: 'route', + }, + ], + [ + 'works with messaging system', + { + [SemanticAttributes.MESSAGING_SYSTEM]: 'test-messaging-system', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'message', + source: 'route', + }, + ], + [ + 'works with faas trigger', + { + [SemanticAttributes.FAAS_TRIGGER]: 'test-faas-trigger', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'test-faas-trigger', + source: 'route', + }, + ], + ])('%s', (_, attributes, name, kind, expected) => { + const actual = parseSpanDescription({ attributes, kind, name } as unknown as Span); + expect(actual).toEqual(expected); + }); +}); + +describe('descriptionForHttpMethod', () => { + it.each([ + [ + 'works withhout attributes', + 'GET', + {}, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'test name', + source: 'custom', + }, + ], + [ + 'works with basic client GET', + 'GET', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_URL]: 'https://www.example.com/my-path', + [SemanticAttributes.HTTP_TARGET]: '/my-path', + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'GET https://www.example.com/my-path', + data: { + url: 'https://www.example.com/my-path', + }, + source: 'url', + }, + ], + [ + 'works with basic server POST', + 'POST', + { + [SemanticAttributes.HTTP_METHOD]: 'POST', + [SemanticAttributes.HTTP_URL]: 'https://www.example.com/my-path', + [SemanticAttributes.HTTP_TARGET]: '/my-path', + }, + 'test name', + SpanKind.SERVER, + { + op: 'http.server', + description: 'POST /my-path', + data: { + url: 'https://www.example.com/my-path', + }, + source: 'url', + }, + ], + [ + 'works with client GET with route', + 'GET', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_URL]: 'https://www.example.com/my-path/123', + [SemanticAttributes.HTTP_TARGET]: '/my-path/123', + [SemanticAttributes.HTTP_ROUTE]: '/my-path/:id', + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'GET /my-path/:id', + data: { + url: 'https://www.example.com/my-path/123', + }, + source: 'route', + }, + ], + ])('%s', (_, httpMethod, attributes, name, kind, expected) => { + const actual = descriptionForHttpMethod({ attributes, kind, name }, httpMethod); + expect(actual).toEqual(expected); + }); +}); + +describe('getSanitizedUrl', () => { + it.each([ + [ + 'works without attributes', + {}, + SpanKind.CLIENT, + { + urlPath: undefined, + url: undefined, + fragment: undefined, + query: undefined, + hasRoute: false, + }, + ], + [ + 'uses url without query for client request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: 'http://example.com/', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: false, + }, + ], + [ + 'uses url without hash for client request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/sub#hash', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/sub#hash', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: 'http://example.com/sub', + url: 'http://example.com/sub', + fragment: '#hash', + query: undefined, + hasRoute: false, + }, + ], + [ + 'uses route if available for client request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_ROUTE]: '/my-route', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: '/my-route', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: true, + }, + ], + [ + 'falls back to target for client request if url not available', + { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.CLIENT, + { + urlPath: '/', + url: undefined, + fragment: undefined, + query: undefined, + hasRoute: false, + }, + ], + [ + 'uses target without query for server request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.SERVER, + { + urlPath: '/', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: false, + }, + ], + [ + 'uses target without hash for server request', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/sub#hash', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.SERVER, + { + urlPath: '/sub', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: false, + }, + ], + [ + 'uses route for server request if available', + { + [SemanticAttributes.HTTP_URL]: 'http://example.com/?what=true', + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_TARGET]: '/?what=true', + [SemanticAttributes.HTTP_ROUTE]: '/my-route', + [SemanticAttributes.HTTP_HOST]: 'example.com:80', + [SemanticAttributes.HTTP_STATUS_CODE]: 200, + }, + SpanKind.SERVER, + { + urlPath: '/my-route', + url: 'http://example.com/', + fragment: undefined, + query: '?what=true', + hasRoute: true, + }, + ], + ])('%s', (_, attributes, kind, expected) => { + const actual = getSanitizedUrl(attributes, kind); + + expect(actual).toEqual(expected); + }); +}); diff --git a/packages/node-experimental/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts similarity index 77% rename from packages/node-experimental/test/utils/setupEventContextTrace.test.ts rename to packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 15d7f0976b9e..704225e4eb20 100644 --- a/packages/node-experimental/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -1,25 +1,24 @@ import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { makeMain } from '@sentry/core'; -import { NodeExperimentalClient } from '../../src/sdk/client'; -import { NodeExperimentalHub } from '../../src/sdk/hub'; -import { setupOtel } from '../../src/sdk/initOtel'; -import { startSpan } from '../../src/sdk/trace'; -import { setupEventContextTrace } from '../../src/utils/setupEventContextTrace'; -import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { OpenTelemetryHub } from '../../src/custom/hub'; +import { setupEventContextTrace } from '../../src/setupEventContextTrace'; +import { setupOtel } from '../helpers/initOtel'; import { cleanupOtel } from '../helpers/mockSdkInit'; +import type { TestClientInterface } from '../helpers/TestClient'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; const PUBLIC_DSN = 'https://username@domain/123'; describe('setupEventContextTrace', () => { const beforeSend = jest.fn(() => null); - let client: NodeExperimentalClient; - let hub: NodeExperimentalHub; + let client: TestClientInterface; + let hub: OpenTelemetryHub; let provider: BasicTracerProvider | undefined; beforeEach(() => { - client = new NodeExperimentalClient( - getDefaultNodeExperimentalClientOptions({ + client = new TestClient( + getDefaultTestClientOptions({ sampleRate: 1, enableTracing: true, beforeSend, @@ -28,7 +27,7 @@ describe('setupEventContextTrace', () => { }), ); - hub = new NodeExperimentalHub(client); + hub = new OpenTelemetryHub(client); makeMain(hub); setupEventContextTrace(client); @@ -74,11 +73,11 @@ describe('setupEventContextTrace', () => { let innerId: string | undefined; let traceId: string | undefined; - startSpan({ name: 'outer' }, outerSpan => { + client.tracer.startActiveSpan('outer', outerSpan => { outerId = outerSpan?.spanContext().spanId; traceId = outerSpan?.spanContext().traceId; - startSpan({ name: 'inner' }, innerSpan => { + client.tracer.startActiveSpan('inner', innerSpan => { innerId = innerSpan?.spanContext().spanId; hub.captureException(error); }); diff --git a/packages/node-experimental/test/utils/spanTypes.test.ts b/packages/opentelemetry/test/utils/spanTypes.test.ts similarity index 72% rename from packages/node-experimental/test/utils/spanTypes.test.ts rename to packages/opentelemetry/test/utils/spanTypes.test.ts index fcd4703db9ce..99152204adfa 100644 --- a/packages/node-experimental/test/utils/spanTypes.test.ts +++ b/packages/opentelemetry/test/utils/spanTypes.test.ts @@ -1,13 +1,6 @@ import type { Span } from '@opentelemetry/api'; -import { - spanHasAttributes, - spanHasEvents, - spanHasKind, - spanHasParentId, - spanIsSdkTraceBaseSpan, -} from '../../src/utils/spanTypes'; -import { createSpan } from '../helpers/createSpan'; +import { spanHasAttributes, spanHasEvents, spanHasKind, spanHasParentId } from '../../src/utils/spanTypes'; describe('spanTypes', () => { describe('spanHasAttributes', () => { @@ -77,22 +70,4 @@ describe('spanTypes', () => { } }); }); - - describe('spanIsSdkTraceBaseSpan', () => { - it.each([ - [{}, false], - [createSpan(), true], - ])('works with %p', (span, expected) => { - const castSpan = span as unknown as Span; - const actual = spanIsSdkTraceBaseSpan(castSpan); - - expect(actual).toBe(expected); - - if (actual) { - expect(castSpan.events).toBeDefined(); - expect(castSpan.attributes).toBeDefined(); - expect(castSpan.kind).toBeDefined(); - } - }); - }); }); diff --git a/packages/opentelemetry/tsconfig.json b/packages/opentelemetry/tsconfig.json new file mode 100644 index 000000000000..bf45a09f2d71 --- /dev/null +++ b/packages/opentelemetry/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + // package-specific options + } +} diff --git a/packages/opentelemetry/tsconfig.test.json b/packages/opentelemetry/tsconfig.test.json new file mode 100644 index 000000000000..87f6afa06b86 --- /dev/null +++ b/packages/opentelemetry/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "jest"] + + // other package-specific, test-specific options + } +} diff --git a/packages/opentelemetry/tsconfig.types.json b/packages/opentelemetry/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/packages/opentelemetry/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index fc0f855fd5d4..fb1d90fbb900 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -36,6 +36,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry/sveltekit', '@sentry-internal/replay-worker', '@sentry/node-experimental', + '@sentry/opentelemetry', '@sentry/vercel-edge', '@sentry/astro', ], @@ -55,6 +56,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry/sveltekit', '@sentry-internal/replay-worker', '@sentry/node-experimental', + '@sentry/opentelemetry', '@sentry/vercel-edge', '@sentry/astro', ], @@ -66,6 +68,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry/remix', '@sentry/sveltekit', '@sentry/node-experimental', + '@sentry/opentelemetry', '@sentry/vercel-edge', '@sentry/astro', ], diff --git a/yarn.lock b/yarn.lock index e4e1855b1d18..3b77a96a282f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4275,10 +4275,10 @@ dependencies: "@opentelemetry/context-base" "^0.14.0" -"@opentelemetry/api@1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.4.1.tgz#ff22eb2e5d476fbc2450a196e40dd243cc20c28f" - integrity sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA== +"@opentelemetry/api@1.6.0", "@opentelemetry/api@^1.6.0", "@opentelemetry/api@~1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.6.0.tgz#de2c6823203d6f319511898bb5de7e70f5267e19" + integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== "@opentelemetry/api@^0.12.0": version "0.12.0" @@ -4287,15 +4287,10 @@ dependencies: "@opentelemetry/context-base" "^0.12.0" -"@opentelemetry/api@^1.6.0", "@opentelemetry/api@~1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.6.0.tgz#de2c6823203d6f319511898bb5de7e70f5267e19" - integrity sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g== - -"@opentelemetry/context-async-hooks@1.17.0", "@opentelemetry/context-async-hooks@~1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.17.0.tgz#d198319785bc81533f1a096e86d7f81ce57346ed" - integrity sha512-bDIRCgpKniSyhORU0fTL9ISW6ucU9nruKyXKwYrEBep/2f3uLz8LFyF51ZUK9QxIwBHw6WJudK/2UqttWzER4w== +"@opentelemetry/context-async-hooks@1.17.1", "@opentelemetry/context-async-hooks@^1.17.1", "@opentelemetry/context-async-hooks@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.17.1.tgz#4eba80bd66f8cd367e9ba94b5fec5f5acf5d7b25" + integrity sha512-up5I+RiQEkGrVEHtbAtmRgS+ZOnFh3shaDNHqZPBlGy+O92auL6yMmjzYpSKmJOGWowvs3fhVHePa8Exb5iHUg== "@opentelemetry/context-base@^0.12.0": version "0.12.0" @@ -4307,20 +4302,20 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== -"@opentelemetry/core@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.15.2.tgz#5b170bf223a2333884bbc2d29d95812cdbda7c9f" - integrity sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw== - dependencies: - "@opentelemetry/semantic-conventions" "1.15.2" - -"@opentelemetry/core@1.17.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.7.0", "@opentelemetry/core@^1.8.0", "@opentelemetry/core@~1.17.0": +"@opentelemetry/core@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.0.tgz#6a72425f5f953dc68b4c7c66d947c018173d30d2" integrity sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg== dependencies: "@opentelemetry/semantic-conventions" "1.17.0" +"@opentelemetry/core@1.17.1", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.17.1", "@opentelemetry/core@^1.8.0", "@opentelemetry/core@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.1.tgz#10c5e09c63aeb1836b34d80baf7113760fb19d96" + integrity sha512-I6LrZvl1FF97FQXPR0iieWQmKnGxYtMbWA1GrAXnLUR+B1Hn2m8KqQNEIlZAucyv00GBgpWkpllmULmZfG8P3g== + dependencies: + "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/core@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-0.12.0.tgz#a888badc9a408fa1f13976a574e69d14be32488e" @@ -4330,17 +4325,17 @@ "@opentelemetry/context-base" "^0.12.0" semver "^7.1.3" -"@opentelemetry/instrumentation-express@~0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.33.1.tgz#0710f839d2a395014d2ffef9390074bb60009841" - integrity sha512-awrpiTZWnLOCJ4TeDMTrs6/gH/oXbNipoPx3WUKQlA1yfMlpNynqokTyCYv1n10Zu9Y2P/nIhoNnUw0ywp61nA== +"@opentelemetry/instrumentation-express@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.33.2.tgz#e5bd14be5814e24b257cd093220d32d5e9261c5a" + integrity sha512-FR05iNosZL42haYang6vpmcuLfXLngJs/0gAgqXk8vwqGGwilOFak1PjoRdO4PAoso0FI+3zhV3Tz7jyDOmSyA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" - "@types/express" "4.17.17" + "@types/express" "4.17.18" -"@opentelemetry/instrumentation-fastify@~0.32.3": +"@opentelemetry/instrumentation-fastify@0.32.3": version "0.32.3" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.32.3.tgz#2c0640c986018d1a41dfff3d9c3bfe3b5b1cf62d" integrity sha512-vRFVoEJXcu6nNpJ61H5syDb84PirOd4b3u8yl8Bcorrr6firGYBQH4pEIVB4PkQWlmi3sLOifqS3VAO2VRloEQ== @@ -4349,91 +4344,80 @@ "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-graphql@~0.35.1": - version "0.35.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.35.1.tgz#e49ec2256bcc4820458688abac0212ac781864c0" - integrity sha512-bAM4W5wU0lZ1UIKK/5b4p8LEU8N6W+VgpcnUIK7GTTDxdhcWTd3Q6oyS6nauhZSzEnAEmmJVXaLQAGIU4sEkyA== +"@opentelemetry/instrumentation-graphql@0.35.2": + version "0.35.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.35.2.tgz#67b0c173cff1191cfa66aa26f67c6752c365edf2" + integrity sha512-lJv7BbHFK0ExwogdQMtVHfnWhCBMDQEz8KYvhShXfRPiSStU5aVwa3TmT0O00KiJFpATSKJNZMv1iZNHbF6z1g== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" -"@opentelemetry/instrumentation-http@~0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.43.0.tgz#c21cf8b407e26c912448b110f340ad1eb657a316" - integrity sha512-Ho3IFQFuD0xmcVc0Uq9AvYvROSOuydn4XWRT/h/GO0VCwOeYz/WCwUJvRdS3m1B3AZ4iGJ0q/nhsATp2JX3/gA== +"@opentelemetry/instrumentation-http@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.44.0.tgz#5a3e4b91073f737f054fe42ef591c39c5b3e6394" + integrity sha512-Nlvj3Y2n9q6uIcQq9f33HbcB4Dr62erSwYA37+vkorYnzI2j9PhxKitocRTZnbYsrymYmQJW9mdq/IAfbtVnNg== dependencies: - "@opentelemetry/core" "1.17.0" - "@opentelemetry/instrumentation" "0.43.0" - "@opentelemetry/semantic-conventions" "1.17.0" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/instrumentation" "0.44.0" + "@opentelemetry/semantic-conventions" "1.17.1" semver "^7.5.2" -"@opentelemetry/instrumentation-mongodb@~0.37.0": - version "0.37.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.37.0.tgz#a7cf4bd9cd5b1053182ec1458c91456812432f55" - integrity sha512-Fwuwf7Fsx/F3QXtU6hbxU4D6DtT33YkAr0+fjtR7qTEcAU0YOxCZfy4tlX2jxjxR1ze8tKfaAWhXBxXwLMWT3g== +"@opentelemetry/instrumentation-mongodb@0.37.1": + version "0.37.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.37.1.tgz#5957565a74a4fe39fb72ab29f3b72a20223ef3df" + integrity sha512-UE+5B/MDfB5MUlJfjj8uo/fMnJPpqeUesJZ/loAWuCLCTDDyEJM7wnAvtH+2c4QoukkkIT1lDe5q9aiXwLEr5g== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/sdk-metrics" "^1.9.1" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-mongoose@~0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.33.1.tgz#0e37eed215fb7fbf8adc0e70199bb8992cb1ea21" - integrity sha512-IzYcEZSmlaOlkyACt8gTl0z3eEQafxzEAt/+W+FdNBiUdm81qpVx/1bpzJwSgIsgcLf27Dl5WsPmrSAi4+Bcng== +"@opentelemetry/instrumentation-mongoose@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.33.2.tgz#99f235df66009e0b73953a58f3f6b9f28e6a31b1" + integrity sha512-JXhhn8vkGKbev6aBPkQ6dL5rDImQfucrub8mU7dknPPpCL850fSQ2qt2qLvyDXfawF5my6KWW0fkKJCeRA+ECw== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-mysql2@~0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.34.1.tgz#d7ce741a7d9a7da270fa791e1c64d8cedd58b5b7" - integrity sha512-SPwgLI2H+gH+GP7b5cWQlFqO/7UeHvw6ZzFKxwLr4vy8wmxYF4aBMLc8qVO8bdXFHd114v0IzOIAvpG6sl/zYQ== +"@opentelemetry/instrumentation-mysql2@0.34.2": + version "0.34.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.34.2.tgz#f59f03c3135a8b50bad9cb3d5b55403008a8d0ba" + integrity sha512-Ac/KAHHtTz087P7I6JapBs+ofNOM+RPTDGwSe1ddnTj0xTAO0F6ITmRC1firnMdzDidI/wI+vmgnWclCB81xKQ== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@opentelemetry/sql-common" "^0.40.0" -"@opentelemetry/instrumentation-mysql@~0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.34.1.tgz#9703d21615dd5ee6b9eda1d74029ba75eec46c9a" - integrity sha512-zQq7hN3ILm1vIJCGeKHRc4pTK8LOmkTt8oKWf0v+whFs7axieIhXZMoCqIBm6BigLy3Trg5iaKyuSrx7kO6q2g== +"@opentelemetry/instrumentation-mysql@0.34.2": + version "0.34.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.34.2.tgz#3372dc11010dce2f357a89a1e3f32359c4d34079" + integrity sha512-3OEhW1CB7b93PHIbQ5t8Aoj/dCqNWQBDBbyUXGy2zFbhEcJBVcLeBpy3w8VEjzNTfRC6cVwASuHRP0aLBIPNjQ== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" - "@types/mysql" "2.15.21" + "@types/mysql" "2.15.22" -"@opentelemetry/instrumentation-nestjs-core@~0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.33.1.tgz#a6e0175bcda25e455339a5527268e746be969297" - integrity sha512-Y5Khvp8ODA6TuDcZKAc63cYDeeZAA/n0ceF0pcVCJwA2NBeD0hmTrCJXES2cvt7wVbHV/SYCu7OpYDQkNjbBWw== +"@opentelemetry/instrumentation-nestjs-core@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.33.2.tgz#fb87031097a96c761db0823c2eff8deba452abbf" + integrity sha512-jrX/355K+myc5V/EQFouqQzBfy5qj+SyVMHIKqVymOx/zWFCvz1p9ChNiPOKzl2il3o/P/aOqBUN/qnRaGowlw== dependencies: - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" -"@opentelemetry/instrumentation-pg@~0.36.1": - version "0.36.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.36.1.tgz#66e3aa10948c6e3188d04676dbf304ae8571ce2f" - integrity sha512-k8L7RSRTQ6e+DbHEXZB8Tmf/efkQnWKeClpZb3TEdb34Pvme4PmcpG2zb6JtM99nNrshNlVDLCZ90U3xDneTbw== +"@opentelemetry/instrumentation-pg@0.36.2": + version "0.36.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.36.2.tgz#45947d19bbafabf5b350a76350ef4523deac13a5" + integrity sha512-KUjI8OGi7kicml2Sd/PR/M8otZoZEdPArMfhznS6OQKit+RxFo0p5x6RVeka/cLQlmoc3eeGBizDeZetssbHgw== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@opentelemetry/sql-common" "^0.40.0" "@types/pg" "8.6.1" - "@types/pg-pool" "2.0.3" + "@types/pg-pool" "2.0.4" -"@opentelemetry/instrumentation@0.41.2", "@opentelemetry/instrumentation@^0.41.2": - version "0.41.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz#cae11fa64485dcf03dae331f35b315b64bc6189f" - integrity sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw== - dependencies: - "@types/shimmer" "^1.0.2" - import-in-the-middle "1.4.2" - require-in-the-middle "^7.1.1" - semver "^7.5.1" - shimmer "^1.2.1" - -"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@^0.43.0", "@opentelemetry/instrumentation@~0.43.0": +"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@^0.43.0": version "0.43.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b" integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== @@ -4444,7 +4428,7 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.44.0": +"@opentelemetry/instrumentation@0.44.0", "@opentelemetry/instrumentation@^0.44.0": version "0.44.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.44.0.tgz#194f16fc96671575b6bd73d3fadffb5aa4497e67" integrity sha512-B6OxJTRRCceAhhnPDBshyQO7K07/ltX3quOLu0icEvPK9QZ7r9P1y0RQX8O5DxB4vTv4URRkxkg+aFU/plNtQw== @@ -4455,29 +4439,21 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/propagator-b3@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.17.0.tgz#32509a8214b7ced7709fd06c0ee5a0d86adcc51f" - integrity sha512-oklstXImtaly4vDaL+rGtX41YXZR50jp5a7CSEPMcStp1B7ozdZ5G2I5wftrDvOlOcLt/TIkGWDCr/OkVN7kWg== - dependencies: - "@opentelemetry/core" "1.17.0" - -"@opentelemetry/propagator-jaeger@1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.17.0.tgz#a89dbc34447db0b5029b719baaa111f268b52265" - integrity sha512-iZzu8K0QkZZ16JH9yox6hZk7/Rxc4SPeGU37pvlB9DtzfNxAEX1FMK9zvowv3ve7r2uzZNpa7JGVUwpy5ewdHQ== +"@opentelemetry/propagator-b3@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.17.1.tgz#65dbddf3763db82632ddd7ad1735e597ab7b2dc4" + integrity sha512-XEbXYb81AM3ayJLlbJqITPIgKBQCuby45ZHiB9mchnmQOffh6ZJOmXONdtZAV7TWzmzwvAd28vGSUk57Aw/5ZA== dependencies: - "@opentelemetry/core" "1.17.0" + "@opentelemetry/core" "1.17.1" -"@opentelemetry/resources@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.15.2.tgz#0c9e26cb65652a1402834a3c030cce6028d6dd9d" - integrity sha512-xmMRLenT9CXmm5HMbzpZ1hWhaUowQf8UB4jMjFlAxx1QzQcsD3KFNAVX/CAWzFPtllTyTplrA4JrQ7sCH3qmYw== +"@opentelemetry/propagator-jaeger@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.17.1.tgz#31cc43045a059d1ed3651b9f21d0fd6db817b02f" + integrity sha512-p+P4lf2pbqd3YMfZO15QCGsDwR2m1ke2q5+dq6YBLa/q0qiC2eq4cD/qhYBBed5/X4PtdamaVGHGsp+u3GXHDA== dependencies: - "@opentelemetry/core" "1.15.2" - "@opentelemetry/semantic-conventions" "1.15.2" + "@opentelemetry/core" "1.17.1" -"@opentelemetry/resources@1.17.0", "@opentelemetry/resources@~1.17.0": +"@opentelemetry/resources@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.0.tgz#ee29144cfd7d194c69698c8153dbadec7fe6819f" integrity sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw== @@ -4485,6 +4461,14 @@ "@opentelemetry/core" "1.17.0" "@opentelemetry/semantic-conventions" "1.17.0" +"@opentelemetry/resources@1.17.1", "@opentelemetry/resources@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.17.1.tgz#932f70f58c0e03fb1d38f0cba12672fd70804d99" + integrity sha512-M2e5emqg5I7qRKqlzKx0ROkcPyF8PbcSaWEdsm72od9txP7Z/Pl8PDYOyu80xWvbHAWk5mDxOF6v3vNdifzclA== + dependencies: + "@opentelemetry/core" "1.17.1" + "@opentelemetry/semantic-conventions" "1.17.1" + "@opentelemetry/resources@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-0.12.0.tgz#5eb287c3032a2bebb2bb9f69b44bd160d2a7d591" @@ -4494,24 +4478,15 @@ "@opentelemetry/core" "^0.12.0" "@opentelemetry/sdk-metrics@^1.9.1": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.17.0.tgz#e51d39e0bb749780d17f9b1df12f0490438dec1a" - integrity sha512-HlWM27yGmYuwCoVRe3yg2PqKnIsq0kEF0HQgvkeDWz2NYkq9fFaSspR6kvjxUTbghAlZrabiqbgyKoYpYaXS3w== + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.17.1.tgz#9c4d13d845bcc82be8684050d9db7cce10f61580" + integrity sha512-eHdpsMCKhKhwznxvEfls8Wv3y4ZBWkkXlD3m7vtHIiWBqsMHspWSfie1s07mM45i/bBCf6YBMgz17FUxIXwmZA== dependencies: - "@opentelemetry/core" "1.17.0" - "@opentelemetry/resources" "1.17.0" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/resources" "1.17.1" lodash.merge "^4.6.2" -"@opentelemetry/sdk-trace-base@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.15.2.tgz#4821f94033c55a6c8bbd35ae387b715b6108517a" - integrity sha512-BEaxGZbWtvnSPchV98qqqqa96AOcb41pjgvhfzDij10tkBhIu9m0Jd6tZ1tJB5ZHfHbTffqYVYE0AOGobec/EQ== - dependencies: - "@opentelemetry/core" "1.15.2" - "@opentelemetry/resources" "1.15.2" - "@opentelemetry/semantic-conventions" "1.15.2" - -"@opentelemetry/sdk-trace-base@1.17.0", "@opentelemetry/sdk-trace-base@^1.17.0", "@opentelemetry/sdk-trace-base@~1.17.0": +"@opentelemetry/sdk-trace-base@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.0.tgz#05a21763c9efa72903c20b8930293cdde344b681" integrity sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ== @@ -4520,28 +4495,37 @@ "@opentelemetry/resources" "1.17.0" "@opentelemetry/semantic-conventions" "1.17.0" -"@opentelemetry/sdk-trace-node@^1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.17.0.tgz#c2f23665e5a6878a7ad6a372dac72e7ab05c4eb5" - integrity sha512-Twlaje+t16b5j62CfcaKU869rP9oyBG/sVQWBI5+kDaWuP/YIFnF4LbovaEahK9GwAnW8vPIn6iYLAl/jZBidA== +"@opentelemetry/sdk-trace-base@1.17.1", "@opentelemetry/sdk-trace-base@^1.17.1", "@opentelemetry/sdk-trace-base@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.17.1.tgz#8ede213df8b0c957028a869c66964e535193a4fd" + integrity sha512-pfSJJSjZj5jkCJUQZicSpzN8Iz9UKMryPWikZRGObPnJo6cUSoKkjZh6BM3j+D47G4olMBN+YZKYqkFM1L6zNA== dependencies: - "@opentelemetry/context-async-hooks" "1.17.0" - "@opentelemetry/core" "1.17.0" - "@opentelemetry/propagator-b3" "1.17.0" - "@opentelemetry/propagator-jaeger" "1.17.0" - "@opentelemetry/sdk-trace-base" "1.17.0" - semver "^7.5.2" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/resources" "1.17.1" + "@opentelemetry/semantic-conventions" "1.17.1" -"@opentelemetry/semantic-conventions@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.15.2.tgz#3bafb5de3e20e841dff6cb3c66f4d6e9694c4241" - integrity sha512-CjbOKwk2s+3xPIMcd5UNYQzsf+v94RczbdNix9/kQh38WiQkM90sUOi3if8eyHFgiBjBjhwXrA7W3ydiSQP9mw== +"@opentelemetry/sdk-trace-node@^1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.17.1.tgz#746c197ad54a8e0cdb24a4b257d33dc3a04493c1" + integrity sha512-J56DaG4cusjw5crpI7x9rv4bxDF27DtKYGxXJF56KIvopbNKpdck5ZWXBttEyqgAVPDwHMAXWDL1KchHzF0a3A== + dependencies: + "@opentelemetry/context-async-hooks" "1.17.1" + "@opentelemetry/core" "1.17.1" + "@opentelemetry/propagator-b3" "1.17.1" + "@opentelemetry/propagator-jaeger" "1.17.1" + "@opentelemetry/sdk-trace-base" "1.17.1" + semver "^7.5.2" -"@opentelemetry/semantic-conventions@1.17.0", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@~1.17.0": +"@opentelemetry/semantic-conventions@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.0.tgz#af10baa9f05ce1e64a14065fc138b5739bfb65f6" integrity sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA== +"@opentelemetry/semantic-conventions@1.17.1", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.17.1", "@opentelemetry/semantic-conventions@~1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.17.1.tgz#93d219935e967fbb9aa0592cc96b2c0ec817a56f" + integrity sha512-xbR2U+2YjauIuo42qmE8XyJK6dYeRMLJuOlUP5SO4auET4VtOHOzgkRVOq+Ik18N+Xf3YPcqJs9dZMiDddz1eQ== + "@opentelemetry/semantic-conventions@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-0.12.0.tgz#7e392aecdbdbd5d737d3995998b120dc17589ab0" @@ -4605,14 +4589,14 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53" integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w== -"@prisma/instrumentation@~5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.3.1.tgz#438b8f1b8b14b190dd7bf1bbc903af4c40856648" - integrity sha512-g4epN9WdsyvX24nuoY7ie9uEsLuNUvYA2ShY9D1Ouz0STMltq1iCWAHugKXYKdKFRtoNP8Vo/QtVLQEEqvNQJQ== +"@prisma/instrumentation@5.4.2": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-5.4.2.tgz#e1615cb50485f029a47e79378d3edac483d6a5f3" + integrity sha512-VSBfo0VS6aY1fIuMBbeLBaTmmgZxszMn2DvHRnGzEnqD/B9/Yfiu96+c0SKuYr7VkuXlbmt5dpbkJutvuJzZBQ== dependencies: - "@opentelemetry/api" "1.4.1" - "@opentelemetry/instrumentation" "0.41.2" - "@opentelemetry/sdk-trace-base" "1.15.2" + "@opentelemetry/api" "1.6.0" + "@opentelemetry/instrumentation" "0.43.0" + "@opentelemetry/sdk-trace-base" "1.17.0" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -5791,10 +5775,10 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/express@4.17.17": - version "4.17.17" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" - integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== +"@types/express@4.17.18": + version "4.17.18" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.18.tgz#efabf5c4495c1880df1bdffee604b143b29c4a95" + integrity sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" @@ -6001,7 +5985,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.32.tgz#f6cd08939ae3ad886fcc92ef7f0109dacddf61ab" integrity sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g== -"@types/mysql@2.15.21", "@types/mysql@^2.15.21": +"@types/mysql@2.15.22": + version "2.15.22" + resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.22.tgz#8705edb9872bf4aa9dbc004cd494e00334e5cdb4" + integrity sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ== + dependencies: + "@types/node" "*" + +"@types/mysql@^2.15.21": version "2.15.21" resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.21.tgz#7516cba7f9d077f980100c85fd500c8210bd5e45" integrity sha512-NPotx5CVful7yB+qZbWtXL2fA4e7aEHkihHLjklc6ID8aq7bhguHgeIoC1EmSNTAuCgI6ZXrjt2ZSaXnYX0EUg== @@ -6073,10 +6064,10 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== -"@types/pg-pool@2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.3.tgz#3eb8df2933f617f219a53091ad4080c94ba1c959" - integrity sha512-fwK5WtG42Yb5RxAwxm3Cc2dJ39FlgcaNiXKvtTLAwtCn642X7dgel+w1+cLWwpSOFImR3YjsZtbkfjxbHtFAeg== +"@types/pg-pool@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.4.tgz#b5c60f678094ff3acf3442628a7f708928fcf263" + integrity sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ== dependencies: "@types/pg" "*" @@ -26989,7 +26980,7 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" -semver@7.5.4, semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: +semver@7.5.4, semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==