Skip to content

Commit c7f21ca

Browse files
authored
feat(node): Add context info for missing instrumentation (#12639)
Adds context to the global scope for missing instrumentation of node frameworks. Fixes #12346
1 parent 3524829 commit c7f21ca

File tree

12 files changed

+134
-1
lines changed

12 files changed

+134
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "node-express-incorrect-instrumentation",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "tsc",
7+
"start": "node dist/app.js",
8+
"test": "playwright test",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
10+
"test:build": "pnpm install && pnpm build",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@sentry/core": "latest || *",
15+
"@sentry/node": "latest || *",
16+
"@sentry/types": "latest || *",
17+
"@trpc/server": "10.45.2",
18+
"@trpc/client": "10.45.2",
19+
"@types/express": "4.17.17",
20+
"@types/node": "18.15.1",
21+
"express": "4.19.2",
22+
"typescript": "4.9.5",
23+
"zod": "~3.22.4"
24+
},
25+
"devDependencies": {
26+
"@playwright/test": "^1.44.1",
27+
"@sentry-internal/test-utils": "link:../../../test-utils"
28+
},
29+
"volta": {
30+
"extends": "../../package.json"
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
declare global {
2+
namespace globalThis {
3+
var transactionIds: string[];
4+
}
5+
}
6+
7+
import express from 'express';
8+
9+
const app = express();
10+
const port = 3030;
11+
12+
// import and init sentry last for missing instrumentation
13+
import * as Sentry from '@sentry/node';
14+
Sentry.init({
15+
environment: 'qa', // dynamic sampling bias to keep transactions
16+
dsn: process.env.E2E_TEST_DSN,
17+
includeLocalVariables: true,
18+
debug: !!process.env.DEBUG,
19+
tunnel: `http://localhost:3031/`, // proxy server
20+
tracesSampleRate: 1,
21+
});
22+
23+
app.get('/test-exception/:id', function (req, _res) {
24+
throw new Error(`This is an exception with id ${req.params.id}`);
25+
});
26+
27+
Sentry.setupExpressErrorHandler(app);
28+
29+
// @ts-ignore
30+
app.use(function onError(err, req, res, next) {
31+
// The error id is attached to `res.sentry` to be returned
32+
// and optionally displayed to the user for support.
33+
res.statusCode = 500;
34+
res.end(res.sentry + '\n');
35+
});
36+
37+
app.listen(port, () => {
38+
console.log(`Example app listening on port ${port}`);
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'node-express',
6+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('Sends correct context when instrumentation was set up incorrectly', async ({ baseURL }) => {
5+
const errorEventPromise = waitForError('node-express', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
7+
});
8+
9+
await fetch(`${baseURL}/test-exception/123`);
10+
11+
const errorEvent = await errorEventPromise;
12+
13+
expect(errorEvent.exception?.values).toHaveLength(1);
14+
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
15+
16+
expect(errorEvent.contexts?.missing_instrumentation).toEqual({
17+
package: 'express',
18+
'javascript.is_cjs': true,
19+
});
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"types": ["node"],
4+
"esModuleInterop": true,
5+
"lib": ["es2018"],
6+
"strict": true,
7+
"outDir": "dist"
8+
},
9+
"include": ["src/**/*.ts"]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { MissingInstrumentationContext } from '@sentry/types';
2+
import { isCjs } from './commonjs';
3+
4+
export const createMissingInstrumentationContext = (pkg: string): MissingInstrumentationContext => ({
5+
package: pkg,
6+
'javascript.is_cjs': isCjs(),
7+
});

packages/node/src/utils/ensureIsWrapped.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { isWrapped } from '@opentelemetry/core';
2-
import { hasTracingEnabled, isEnabled } from '@sentry/core';
2+
import { getGlobalScope, hasTracingEnabled, isEnabled } from '@sentry/core';
33
import { consoleSandbox } from '@sentry/utils';
44
import { isCjs } from './commonjs';
5+
import { createMissingInstrumentationContext } from './createMissingInstrumentationContext';
56

67
/**
78
* Checks and warns if a framework isn't wrapped by opentelemetry.
@@ -24,5 +25,7 @@ export function ensureIsWrapped(
2425
);
2526
}
2627
});
28+
29+
getGlobalScope().setContext('missing_instrumentation', createMissingInstrumentationContext(name));
2730
}
2831
}

packages/types/src/context.ts

+5
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,8 @@ export interface CloudResourceContext extends Record<string, unknown> {
119119
export interface ProfileContext extends Record<string, unknown> {
120120
profile_id: string;
121121
}
122+
123+
export interface MissingInstrumentationContext extends Record<string, unknown> {
124+
package: string;
125+
['javascript.is_cjs']?: boolean;
126+
}

packages/types/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type {
1818
CultureContext,
1919
TraceContext,
2020
CloudResourceContext,
21+
MissingInstrumentationContext,
2122
} from './context';
2223
export type { DataCategory } from './datacategory';
2324
export type { DsnComponents, DsnLike, DsnProtocol } from './dsn';

0 commit comments

Comments
 (0)