Skip to content

Commit e45d587

Browse files
committed
update otel resources usage in edge
1 parent 29ed4da commit e45d587

3 files changed

Lines changed: 166 additions & 75 deletions

File tree

packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions';
2-
import { GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span, type SpanAttributes } from '@sentry/core';
3-
import { isSentryRequestSpan } from '@sentry/opentelemetry';
2+
import {
3+
getClient,
4+
GLOBAL_OBJ,
5+
isSentryRequestUrl,
6+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
7+
type Span,
8+
type SpanAttributes,
9+
} from '@sentry/core';
410
import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes';
511
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached';
612

@@ -36,6 +42,36 @@ export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes |
3642
}
3743
}
3844

45+
/**
46+
* Local copy of `@sentry/opentelemetry`'s `isSentryRequestSpan`, to avoid pulling the whole package into Edge bundles.
47+
*/
48+
function isSentryRequestSpan(span: Span): boolean {
49+
const attributes = spanToAttributes(span);
50+
if (!attributes) {
51+
return false;
52+
}
53+
54+
const httpUrl = attributes['http.url'] || attributes['url.full'];
55+
if (!httpUrl) {
56+
return false;
57+
}
58+
59+
return isSentryRequestUrl(httpUrl.toString(), getClient());
60+
}
61+
62+
function spanToAttributes(span: Span): Record<string, unknown> | undefined {
63+
// OTEL spans expose attributes in different shapes depending on implementation.
64+
// We only need best-effort read access.
65+
type MaybeSpanAttributes = {
66+
attributes?: Record<string, unknown>;
67+
_attributes?: Record<string, unknown>;
68+
};
69+
70+
const maybeSpan = span as unknown as MaybeSpanAttributes;
71+
const attrs = maybeSpan.attributes || maybeSpan._attributes;
72+
return attrs;
73+
}
74+
3975
/**
4076
* Checks if a span's HTTP target matches the tunnel route.
4177
*/

packages/nextjs/src/edge/index.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// import/export got a false positive, and affects most of our index barrel files
22
// can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703
33
/* eslint-disable import/export */
4-
import { context } from '@opentelemetry/api';
4+
import { context, createContextKey } from '@opentelemetry/api';
55
import {
66
applySdkMetadata,
77
type EventProcessor,
@@ -12,14 +12,14 @@ import {
1212
getRootSpan,
1313
GLOBAL_OBJ,
1414
registerSpanErrorInstrumentation,
15+
type Scope,
1516
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1617
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1718
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1819
setCapturedScopesOnSpan,
1920
spanToJSON,
2021
stripUrlQueryAndFragment,
2122
} from '@sentry/core';
22-
import { getScopesFromContext } from '@sentry/opentelemetry';
2323
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
2424
import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';
2525
import { DEBUG_BUILD } from '../common/debug-build';
@@ -42,6 +42,32 @@ export { wrapApiHandlerWithSentry } from './wrapApiHandlerWithSentry';
4242

4343
export type EdgeOptions = VercelEdgeOptions;
4444

45+
type CurrentScopes = {
46+
scope: Scope;
47+
isolationScope: Scope;
48+
};
49+
50+
// This key must match `@sentry/opentelemetry`'s `SENTRY_SCOPES_CONTEXT_KEY`.
51+
// We duplicate it here so the Edge bundle does not need to import the full `@sentry/opentelemetry` package.
52+
const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes');
53+
54+
type ContextWithGetValue = {
55+
getValue(key: unknown): unknown;
56+
};
57+
58+
function getScopesFromContext(otelContext: unknown): CurrentScopes | undefined {
59+
if (!otelContext || typeof otelContext !== 'object') {
60+
return undefined;
61+
}
62+
63+
const maybeContext = otelContext as Partial<ContextWithGetValue>;
64+
if (typeof maybeContext.getValue !== 'function') {
65+
return undefined;
66+
}
67+
68+
return maybeContext.getValue(SENTRY_SCOPES_CONTEXT_KEY) as CurrentScopes | undefined;
69+
}
70+
4571
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
4672
_sentryRewriteFramesDistDir?: string;
4773
_sentryRelease?: string;
Lines changed: 100 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,108 @@
11
import replace from '@rollup/plugin-replace';
22
import { makeBaseNPMConfig, makeNPMConfigVariants, plugins } from '@sentry-internal/rollup-utils';
33

4-
export default makeNPMConfigVariants(
5-
makeBaseNPMConfig({
6-
entrypoints: ['src/index.ts'],
7-
bundledBuiltins: ['perf_hooks', 'util'],
8-
packageSpecificConfig: {
9-
context: 'globalThis',
10-
output: {
11-
preserveModules: false,
12-
},
13-
plugins: [
14-
plugins.makeCommonJSPlugin({ transformMixedEsModules: true }), // Needed because various modules in the OTEL toolchain use CJS (require-in-the-middle, shimmer, etc..)
15-
plugins.makeJsonPlugin(), // Needed because `require-in-the-middle` imports json via require
16-
replace({
17-
preventAssignment: true,
18-
values: {
19-
'process.argv0': JSON.stringify(''), // needed because otel relies on process.argv0 for the default service name, but that api is not available in the edge runtime.
20-
},
21-
}),
22-
{
23-
// This plugin is needed because otel imports `performance` from `perf_hooks` and also uses it via the `performance` global.
24-
// It also imports `inspect` and `promisify` from node's `util` which are not available in the edge runtime so we need to define a polyfill.
25-
// Both of these APIs are not available in the edge runtime so we need to define a polyfill.
26-
// Vercel does something similar in the `@vercel/otel` package: https://github.com/vercel/otel/blob/087601ae585cb116bb2b46c211d014520de76c71/packages/otel/build.ts#L62
27-
name: 'edge-runtime-polyfills',
28-
banner: `
29-
{
30-
if (globalThis.performance === undefined) {
31-
globalThis.performance = {
32-
timeOrigin: 0,
33-
now: () => Date.now()
34-
};
35-
}
36-
}
37-
`,
38-
resolveId: source => {
39-
if (source === 'perf_hooks') {
40-
return '\0perf_hooks_sentry_shim';
41-
} else if (source === 'util') {
42-
return '\0util_sentry_shim';
43-
} else {
44-
return null;
4+
const baseConfig = makeBaseNPMConfig({
5+
entrypoints: ['src/index.ts'],
6+
bundledBuiltins: ['perf_hooks', 'util'],
7+
packageSpecificConfig: {
8+
context: 'globalThis',
9+
output: {
10+
preserveModules: false,
11+
},
12+
plugins: [
13+
plugins.makeCommonJSPlugin({ transformMixedEsModules: true }), // Needed because various modules in the OTEL toolchain use CJS (require-in-the-middle, shimmer, etc..)
14+
plugins.makeJsonPlugin(), // Needed because `require-in-the-middle` imports json via require
15+
replace({
16+
preventAssignment: true,
17+
values: {
18+
'process.argv0': JSON.stringify(''), // needed because otel relies on process.argv0 for the default service name, but that api is not available in the edge runtime.
19+
},
20+
}),
21+
{
22+
// This plugin is needed because otel imports `performance` from `perf_hooks` and also uses it via the `performance` global.
23+
// It also imports `inspect` and `promisify` from node's `util` which are not available in the edge runtime so we need to define a polyfill.
24+
// Both of these APIs are not available in the edge runtime so we need to define a polyfill.
25+
// Vercel does something similar in the `@vercel/otel` package: https://github.com/vercel/otel/blob/087601ae585cb116bb2b46c211d014520de76c71/packages/otel/build.ts#L62
26+
name: 'edge-runtime-polyfills',
27+
banner: `
28+
{
29+
if (globalThis.performance === undefined) {
30+
globalThis.performance = {
31+
timeOrigin: 0,
32+
now: () => Date.now()
33+
};
4534
}
46-
},
47-
load: id => {
48-
if (id === '\0perf_hooks_sentry_shim') {
49-
return `
50-
export const performance = {
51-
timeOrigin: 0,
52-
now: () => Date.now()
53-
}
54-
`;
55-
} else if (id === '\0util_sentry_shim') {
56-
return `
57-
export const inspect = (object) =>
58-
JSON.stringify(object, null, 2);
35+
}
36+
`,
37+
resolveId: source => {
38+
if (source === 'perf_hooks') {
39+
return '\0perf_hooks_sentry_shim';
40+
} else if (source === 'util') {
41+
return '\0util_sentry_shim';
42+
} else {
43+
return null;
44+
}
45+
},
46+
load: id => {
47+
if (id === '\0perf_hooks_sentry_shim') {
48+
return `
49+
export const performance = {
50+
timeOrigin: 0,
51+
now: () => Date.now()
52+
}
53+
`;
54+
} else if (id === '\0util_sentry_shim') {
55+
return `
56+
export const inspect = (object) =>
57+
JSON.stringify(object, null, 2);
5958
60-
export const promisify = (fn) => {
61-
return (...args) => {
62-
return new Promise((resolve, reject) => {
63-
fn(...args, (err, result) => {
64-
if (err) reject(err);
65-
else resolve(result);
66-
});
59+
export const promisify = (fn) => {
60+
return (...args) => {
61+
return new Promise((resolve, reject) => {
62+
fn(...args, (err, result) => {
63+
if (err) reject(err);
64+
else resolve(result);
6765
});
68-
};
66+
});
6967
};
70-
`;
71-
} else {
72-
return null;
73-
}
74-
},
68+
};
69+
`;
70+
} else {
71+
return null;
72+
}
7573
},
76-
],
77-
},
78-
}),
79-
);
74+
},
75+
],
76+
},
77+
});
78+
79+
// `makeBaseNPMConfig` marks dependencies/peers as external by default.
80+
// For Edge, we must ensure the OTEL SDK bits which reference `process.argv0` are bundled so our replace() plugin applies.
81+
const baseExternal = baseConfig.external;
82+
baseConfig.external = (source, importer, isResolved) => {
83+
// Never treat these as external - they need to be inlined so `process.argv0` can be replaced.
84+
if (
85+
source === '@opentelemetry/resources' ||
86+
source.startsWith('@opentelemetry/resources/') ||
87+
source === '@opentelemetry/sdk-trace-base' ||
88+
source.startsWith('@opentelemetry/sdk-trace-base/')
89+
) {
90+
return false;
91+
}
92+
93+
if (typeof baseExternal === 'function') {
94+
return baseExternal(source, importer, isResolved);
95+
}
96+
97+
if (Array.isArray(baseExternal)) {
98+
return baseExternal.includes(source);
99+
}
100+
101+
if (baseExternal instanceof RegExp) {
102+
return baseExternal.test(source);
103+
}
104+
105+
return false;
106+
};
107+
108+
export default makeNPMConfigVariants(baseConfig);

0 commit comments

Comments
 (0)