diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.gitignore b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.gitignore @@ -0,0 +1 @@ +dist diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.npmrc b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json new file mode 100644 index 000000000000..3df947bb58c5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/package.json @@ -0,0 +1,32 @@ +{ + "name": "node-express-incorrect-instrumentation", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/core": "latest || *", + "@sentry/node": "latest || *", + "@sentry/types": "latest || *", + "@trpc/server": "10.45.2", + "@trpc/client": "10.45.2", + "@types/express": "4.17.17", + "@types/node": "18.15.1", + "express": "4.19.2", + "typescript": "4.9.5", + "zod": "~3.22.4" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/src/app.ts new file mode 100644 index 000000000000..2ab5d1ace5a0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/src/app.ts @@ -0,0 +1,39 @@ +declare global { + namespace globalThis { + var transactionIds: string[]; + } +} + +import express from 'express'; + +const app = express(); +const port = 3030; + +// import and init sentry last for missing instrumentation +import * as Sentry from '@sentry/node'; +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + includeLocalVariables: true, + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, +}); + +app.get('/test-exception/:id', function (req, _res) { + throw new Error(`This is an exception with id ${req.params.id}`); +}); + +Sentry.setupExpressErrorHandler(app); + +// @ts-ignore +app.use(function onError(err, req, res, next) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/start-event-proxy.mjs new file mode 100644 index 000000000000..3276781a442a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-express', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tests/instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tests/instrumentation.test.ts new file mode 100644 index 000000000000..f562cb3a5837 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tests/instrumentation.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends correct context when instrumentation was set up incorrectly', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-express', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + await fetch(`${baseURL}/test-exception/123`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.contexts?.missing_instrumentation).toEqual({ + package: 'express', + 'javascript.is_cjs': true, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tsconfig.json new file mode 100644 index 000000000000..8cb64e989ed9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-express-incorrect-instrumentation/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2018"], + "strict": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/node/src/utils/createMissingInstrumentationContext.ts b/packages/node/src/utils/createMissingInstrumentationContext.ts new file mode 100644 index 000000000000..849e49e8b8e2 --- /dev/null +++ b/packages/node/src/utils/createMissingInstrumentationContext.ts @@ -0,0 +1,7 @@ +import type { MissingInstrumentationContext } from '@sentry/types'; +import { isCjs } from './commonjs'; + +export const createMissingInstrumentationContext = (pkg: string): MissingInstrumentationContext => ({ + package: pkg, + 'javascript.is_cjs': isCjs(), +}); diff --git a/packages/node/src/utils/ensureIsWrapped.ts b/packages/node/src/utils/ensureIsWrapped.ts index 8cf85b39d545..05185a293bed 100644 --- a/packages/node/src/utils/ensureIsWrapped.ts +++ b/packages/node/src/utils/ensureIsWrapped.ts @@ -1,7 +1,8 @@ import { isWrapped } from '@opentelemetry/core'; -import { hasTracingEnabled, isEnabled } from '@sentry/core'; +import { getGlobalScope, hasTracingEnabled, isEnabled } from '@sentry/core'; import { consoleSandbox } from '@sentry/utils'; import { isCjs } from './commonjs'; +import { createMissingInstrumentationContext } from './createMissingInstrumentationContext'; /** * Checks and warns if a framework isn't wrapped by opentelemetry. @@ -24,5 +25,7 @@ export function ensureIsWrapped( ); } }); + + getGlobalScope().setContext('missing_instrumentation', createMissingInstrumentationContext(name)); } } diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 0344dd179787..10fc61420e25 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -119,3 +119,8 @@ export interface CloudResourceContext extends Record { export interface ProfileContext extends Record { profile_id: string; } + +export interface MissingInstrumentationContext extends Record { + package: string; + ['javascript.is_cjs']?: boolean; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a7cf18056eb6..8f7fdce74c33 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -18,6 +18,7 @@ export type { CultureContext, TraceContext, CloudResourceContext, + MissingInstrumentationContext, } from './context'; export type { DataCategory } from './datacategory'; export type { DsnComponents, DsnLike, DsnProtocol } from './dsn';