From aff9240f4a3d7931d92b557e32e23deeeeeae322 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 27 Mar 2024 17:08:28 +0000 Subject: [PATCH] feat: Add mechanism to promote spans to transactions --- packages/nextjs/package.json | 2 + packages/nextjs/src/server/index.ts | 5 +- .../src/server/promoteHttpSpansIntegration.ts | 17 ++++++ .../opentelemetry/src/semanticAttributes.ts | 3 ++ packages/opentelemetry/src/spanExporter.ts | 8 ++- yarn.lock | 53 ++++++++++++++++--- 6 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 packages/nextjs/src/server/promoteHttpSpansIntegration.ts diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 213958d1fd84..9a8af52bec20 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -35,12 +35,14 @@ "access": "public" }, "dependencies": { + "@opentelemetry/api": "1.7.0", "@rollup/plugin-commonjs": "24.0.0", "@sentry/core": "8.0.0-alpha.7", "@sentry/node": "8.0.0-alpha.7", "@sentry/react": "8.0.0-alpha.7", "@sentry/types": "8.0.0-alpha.7", "@sentry/utils": "8.0.0-alpha.7", + "@sentry/opentelemetry": "8.0.0-alpha.7", "@sentry/vercel-edge": "8.0.0-alpha.7", "@sentry/webpack-plugin": "2.16.0", "chalk": "3.0.0", diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 4dc6910d49eb..7a6e5248d3c2 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -12,6 +12,7 @@ import { onUncaughtExceptionIntegration } from './onUncaughtExceptionIntegration export * from '@sentry/node'; import type { EventProcessor } from '@sentry/types'; +import { promoteHttpSpansIntegration } from './promoteHttpSpansIntegration'; export { captureUnderscoreErrorException } from '../common/_error'; export { onUncaughtExceptionIntegration } from './onUncaughtExceptionIntegration'; @@ -76,8 +77,10 @@ export function init(options: NodeOptions): void { integration => integration.name !== 'OnUncaughtException' && // Next.js comes with its own Node-Fetch instrumentation so we shouldn't add ours on-top - integration.name !== 'NodeFetch', + integration.name !== 'NodeFetch' && + integration.name !== 'Http', ), + promoteHttpSpansIntegration(), onUncaughtExceptionIntegration(), ]; diff --git a/packages/nextjs/src/server/promoteHttpSpansIntegration.ts b/packages/nextjs/src/server/promoteHttpSpansIntegration.ts new file mode 100644 index 000000000000..2732020f8074 --- /dev/null +++ b/packages/nextjs/src/server/promoteHttpSpansIntegration.ts @@ -0,0 +1,17 @@ +import { SpanKind } from '@opentelemetry/api'; +import { defineIntegration, spanToJSON } from '@sentry/core'; +import { getSpanKind } from '@sentry/opentelemetry'; + +export const promoteHttpSpansIntegration = defineIntegration(() => ({ + name: 'PromoteHttpSpansIntegration', + setup(client) { + client.on('spanStart', span => { + const spanJson = spanToJSON(span); + + // The following check is a heuristic to determine whether the started span is a span that tracks an incoming HTTP request + if (getSpanKind(span) === SpanKind.SERVER && spanJson.data && 'http.method' in spanJson.data) { + span.setAttribute('sentry.promoteToTransaction', true); + } + }); + }, +})); diff --git a/packages/opentelemetry/src/semanticAttributes.ts b/packages/opentelemetry/src/semanticAttributes.ts index 80a80f87a666..992b82e94675 100644 --- a/packages/opentelemetry/src/semanticAttributes.ts +++ b/packages/opentelemetry/src/semanticAttributes.ts @@ -1,2 +1,5 @@ /** If this attribute is true, it means that the parent is a remote span. */ export const SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE = 'sentry.parentIsRemote'; + +/** If this attribute is true, it means that the span should be promoted to a transaction before being sent to sentry. */ +export const SEMANTIC_ATTRIBUTE_SENTRY_PROMOTE_TO_TRANSACTION = 'sentry.promoteToTransaction'; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index c7cfbd36f261..f455adb4b71e 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -14,7 +14,10 @@ import type { SpanJSON, SpanOrigin, TraceContext, TransactionEvent, TransactionS import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; -import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE, + SEMANTIC_ATTRIBUTE_SENTRY_PROMOTE_TO_TRANSACTION, +} from './semanticAttributes'; import { convertOtelTimeToSeconds } from './utils/convertOtelTimeToSeconds'; import { getDynamicSamplingContextFromSpan } from './utils/dynamicSamplingContext'; import { getRequestSpanData } from './utils/getRequestSpanData'; @@ -149,7 +152,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { } function nodeIsCompletedRootNode(node: SpanNode): node is SpanNodeCompleted { - return !!node.span && !node.parentNode; + return (!!node.span && !node.parentNode) || !!node.span?.attributes[SEMANTIC_ATTRIBUTE_SENTRY_PROMOTE_TO_TRANSACTION]; } function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { @@ -319,6 +322,7 @@ function removeSentryAttributes(data: Record): Record