Skip to content

Commit 9bc4bc8

Browse files
author
Luca Forstner
authored
Merge branch 'develop' into lforst-set-headers-instead-of-append
2 parents 5e01be3 + 067ad93 commit 9bc4bc8

File tree

13 files changed

+214
-64
lines changed

13 files changed

+214
-64
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ module.exports = [
180180
name: 'CDN Bundle (incl. Tracing, Replay)',
181181
path: createCDNPath('bundle.tracing.replay.min.js'),
182182
gzip: true,
183-
limit: '73.1 KB',
183+
limit: '74 KB',
184184
},
185185
{
186186
name: 'CDN Bundle (incl. Tracing, Replay, Feedback)',

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
99
## Unreleased
1010

11+
### Important Changes
12+
13+
- **ref(nextjs): Remove dead code ([#13828](https://github.com/getsentry/sentry-javascript/pull/13903))**
14+
15+
Relevant for users of the `@sentry/nextjs` package: If you have previously configured a
16+
`SENTRY_IGNORE_API_RESOLUTION_ERROR` environment variable, it is now safe to unset it.
17+
1118
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1219

1320
Work in this release was contributed by @trzeciak and @lizhiyao. Thank you for your contributions!
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window._triggerError = function (errorCount) {
4+
Sentry.captureException(new Error(`This is error #${errorCount}`));
5+
};

dev-packages/browser-integration-tests/suites/replay/dsc/test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import type * as Sentry from '@sentry/browser';
33
import type { EventEnvelopeHeaders } from '@sentry/types';
44

55
import { sentryTest } from '../../../utils/fixtures';
6-
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';
6+
import {
7+
envelopeRequestParser,
8+
shouldSkipTracingTest,
9+
waitForErrorRequest,
10+
waitForTransactionRequest,
11+
} from '../../../utils/helpers';
712
import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRunning } from '../../../utils/replayHelpers';
813

914
type TestWindow = Window & {
@@ -216,3 +221,77 @@ sentryTest(
216221
});
217222
},
218223
);
224+
225+
sentryTest('should add replay_id to error DSC while replay is active', async ({ getLocalTestPath, page }) => {
226+
if (shouldSkipReplayTest()) {
227+
sentryTest.skip();
228+
}
229+
230+
const hasTracing = !shouldSkipTracingTest();
231+
232+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
233+
return route.fulfill({
234+
status: 200,
235+
contentType: 'application/json',
236+
body: JSON.stringify({ id: 'test-id' }),
237+
});
238+
});
239+
240+
const url = await getLocalTestPath({ testDir: __dirname });
241+
await page.goto(url);
242+
243+
const error1Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #1');
244+
const error2Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #2');
245+
246+
// We want to wait for the transaction to be done, to ensure we have a consistent test
247+
const transactionReq = hasTracing ? waitForTransactionRequest(page) : Promise.resolve();
248+
249+
// Wait for this to be available
250+
await page.waitForFunction('!!window.Replay');
251+
252+
// We have to start replay before we finish the transaction, otherwise the DSC will not be frozen with the Replay ID
253+
await page.evaluate('window.Replay.start();');
254+
await waitForReplayRunning(page);
255+
await transactionReq;
256+
257+
await page.evaluate('window._triggerError(1)');
258+
259+
const error1Header = envelopeRequestParser(await error1Req, 0) as EventEnvelopeHeaders;
260+
const replay = await getReplaySnapshot(page);
261+
262+
expect(replay.session?.id).toBeDefined();
263+
264+
expect(error1Header.trace).toBeDefined();
265+
expect(error1Header.trace).toEqual({
266+
environment: 'production',
267+
trace_id: expect.any(String),
268+
public_key: 'public',
269+
replay_id: replay.session?.id,
270+
...(hasTracing
271+
? {
272+
sample_rate: '1',
273+
sampled: 'true',
274+
}
275+
: {}),
276+
});
277+
278+
// Now end replay and trigger another error, it should not have a replay_id in DSC anymore
279+
await page.evaluate('window.Replay.stop();');
280+
await page.waitForFunction('!window.Replay.getReplayId();');
281+
await page.evaluate('window._triggerError(2)');
282+
283+
const error2Header = envelopeRequestParser(await error2Req, 0) as EventEnvelopeHeaders;
284+
285+
expect(error2Header.trace).toBeDefined();
286+
expect(error2Header.trace).toEqual({
287+
environment: 'production',
288+
trace_id: expect.any(String),
289+
public_key: 'public',
290+
...(hasTracing
291+
? {
292+
sample_rate: '1',
293+
sampled: 'true',
294+
}
295+
: {}),
296+
});
297+
});

dev-packages/browser-integration-tests/utils/helpers.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export async function waitForTransactionRequestOnUrl(page: Page, url: string): P
199199
return req;
200200
}
201201

202-
export function waitForErrorRequest(page: Page): Promise<Request> {
202+
export function waitForErrorRequest(page: Page, callback?: (event: Event) => boolean): Promise<Request> {
203203
return page.waitForRequest(req => {
204204
const postData = req.postData();
205205
if (!postData) {
@@ -209,7 +209,15 @@ export function waitForErrorRequest(page: Page): Promise<Request> {
209209
try {
210210
const event = envelopeRequestParser(req);
211211

212-
return !event.type;
212+
if (event.type) {
213+
return false;
214+
}
215+
216+
if (callback) {
217+
return callback(event);
218+
}
219+
220+
return true;
213221
} catch {
214222
return false;
215223
}

packages/nextjs/src/common/types.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@ export type WrappedNextApiHandler = {
5555
__sentry_route__?: string;
5656
__sentry_wrapped__?: boolean;
5757
};
58-
59-
export type AugmentedNextApiRequest = NextApiRequest & {
60-
__withSentry_applied__?: boolean;
61-
};
62-
6358
export type AugmentedNextApiResponse = NextApiResponse & {
6459
__sentryTransaction?: SentrySpan;
6560
};

packages/nextjs/src/common/wrapApiHandlerWithSentry.ts

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ import {
66
startSpanManual,
77
withIsolationScope,
88
} from '@sentry/core';
9-
import { consoleSandbox, isString, logger, objectify, vercelWaitUntil } from '@sentry/utils';
10-
119
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
12-
import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler } from './types';
10+
import { isString, logger, objectify, vercelWaitUntil } from '@sentry/utils';
11+
import type { NextApiRequest } from 'next';
12+
import type { AugmentedNextApiResponse, NextApiHandler } from './types';
1313
import { flushSafelyWithTimeout } from './utils/responseEnd';
1414
import { escapeNextjsTracing } from './utils/tracingUtils';
1515

16+
export type AugmentedNextApiRequest = NextApiRequest & {
17+
__withSentry_applied__?: boolean;
18+
};
19+
1620
/**
17-
* Wrap the given API route handler for tracing and error capturing. Thin wrapper around `withSentry`, which only
18-
* applies it if it hasn't already been applied.
21+
* Wrap the given API route handler with error nad performance monitoring.
1922
*
2023
* @param apiHandler The handler exported from the user's API page route file, which may or may not already be
2124
* wrapped with `withSentry`
2225
* @param parameterizedRoute The page's parameterized route.
23-
* @returns The wrapped handler
26+
* @returns The wrapped handler which will always return a Promise.
2427
*/
2528
export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameterizedRoute: string): NextApiHandler {
2629
return new Proxy(apiHandler, {
@@ -44,9 +47,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
4447
return wrappingTarget.apply(thisArg, args);
4548
}
4649

47-
// We're now auto-wrapping API route handlers using `wrapApiHandlerWithSentry` (which uses `withSentry` under the hood), but
48-
// users still may have their routes manually wrapped with `withSentry`. This check makes `sentryWrappedHandler`
49-
// idempotent so that those cases don't break anything.
50+
// Prevent double wrapping of the same request.
5051
if (req.__withSentry_applied__) {
5152
return wrappingTarget.apply(thisArg, args);
5253
}
@@ -55,7 +56,6 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
5556
return withIsolationScope(isolationScope => {
5657
return continueTrace(
5758
{
58-
// TODO(v8): Make it so that continue trace will allow null as sentryTrace value and remove this fallback here
5959
sentryTrace:
6060
req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined,
6161
baggage: req.headers?.baggage,
@@ -80,31 +80,15 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
8080
// eslint-disable-next-line @typescript-eslint/unbound-method
8181
res.end = new Proxy(res.end, {
8282
apply(target, thisArg, argArray) {
83-
if (span.isRecording()) {
84-
setHttpStatus(span, res.statusCode);
85-
span.end();
86-
}
83+
setHttpStatus(span, res.statusCode);
84+
span.end();
8785
vercelWaitUntil(flushSafelyWithTimeout());
88-
target.apply(thisArg, argArray);
86+
return target.apply(thisArg, argArray);
8987
},
9088
});
9189

9290
try {
93-
const handlerResult = await wrappingTarget.apply(thisArg, args);
94-
if (
95-
process.env.NODE_ENV === 'development' &&
96-
!process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR &&
97-
!res.writableEnded
98-
) {
99-
consoleSandbox(() => {
100-
// eslint-disable-next-line no-console
101-
console.warn(
102-
'[sentry] If Next.js logs a warning "API resolved without sending a response", it\'s a false positive, which may happen when you use `wrapApiHandlerWithSentry` manually to wrap your routes. To suppress this warning, set `SENTRY_IGNORE_API_RESOLUTION_ERROR` to 1 in your env. To suppress the nextjs warning, use the `externalResolver` API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).',
103-
);
104-
});
105-
}
106-
107-
return handlerResult;
91+
return await wrappingTarget.apply(thisArg, args);
10892
} catch (e) {
10993
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
11094
// store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced
@@ -123,16 +107,8 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
123107
},
124108
});
125109

126-
// Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet
127-
// have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that
128-
// the transaction was error-free
129-
res.statusCode = 500;
130-
res.statusMessage = 'Internal Server Error';
131-
132-
if (span.isRecording()) {
133-
setHttpStatus(span, res.statusCode);
134-
span.end();
135-
}
110+
setHttpStatus(span, 500);
111+
span.end();
136112

137113
vercelWaitUntil(flushSafelyWithTimeout());
138114

packages/nextjs/src/server/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { GLOBAL_OBJ, logger } from '@sentry/utils';
1414
import {
1515
ATTR_HTTP_REQUEST_METHOD,
1616
ATTR_HTTP_ROUTE,
17+
ATTR_URL_QUERY,
1718
SEMATTRS_HTTP_METHOD,
1819
SEMATTRS_HTTP_TARGET,
1920
} from '@opentelemetry/semantic-conventions';
@@ -157,11 +158,14 @@ export function init(options: NodeOptions): NodeClient | undefined {
157158
// We need to drop these spans.
158159
if (
159160
// eslint-disable-next-line deprecation/deprecation
160-
typeof spanAttributes[SEMATTRS_HTTP_TARGET] === 'string' &&
161-
// eslint-disable-next-line deprecation/deprecation
162-
spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_key') &&
163-
// eslint-disable-next-line deprecation/deprecation
164-
spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_client')
161+
(typeof spanAttributes[SEMATTRS_HTTP_TARGET] === 'string' &&
162+
// eslint-disable-next-line deprecation/deprecation
163+
spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_key') &&
164+
// eslint-disable-next-line deprecation/deprecation
165+
spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_client')) ||
166+
(typeof spanAttributes[ATTR_URL_QUERY] === 'string' &&
167+
spanAttributes[ATTR_URL_QUERY].includes('sentry_key') &&
168+
spanAttributes[ATTR_URL_QUERY].includes('sentry_client'))
165169
) {
166170
samplingDecision.decision = false;
167171
}
@@ -238,8 +242,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
238242
// Pages router
239243
event.transaction === '/404' ||
240244
// App router (could be "GET /404", "POST /404", ...)
241-
event.transaction?.match(/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/404$/) ||
242-
event.transaction === 'GET /_not-found'
245+
event.transaction?.match(/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/)
243246
) {
244247
return null;
245248
}

packages/node/src/integrations/tracing/graphql.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,7 @@ const INTEGRATION_NAME = 'Graphql';
4040
export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
4141
INTEGRATION_NAME,
4242
(_options: GraphqlOptions = {}) => {
43-
const options = {
44-
ignoreResolveSpans: true,
45-
ignoreTrivialResolveSpans: true,
46-
useOperationNameForRootSpan: true,
47-
..._options,
48-
};
43+
const options = getOptionsWithDefaults(_options);
4944

5045
return new GraphQLInstrumentation({
5146
...options,
@@ -89,7 +84,10 @@ const _graphqlIntegration = ((options: GraphqlOptions = {}) => {
8984
return {
9085
name: INTEGRATION_NAME,
9186
setupOnce() {
92-
instrumentGraphql(options);
87+
// We set defaults here, too, because otherwise we'd update the instrumentation config
88+
// to the config without defaults, as `generateInstrumentOnce` automatically calls `setConfig(options)`
89+
// when being called the second time
90+
instrumentGraphql(getOptionsWithDefaults(options));
9391
},
9492
};
9593
}) satisfies IntegrationFn;
@@ -100,3 +98,12 @@ const _graphqlIntegration = ((options: GraphqlOptions = {}) => {
10098
* Capture tracing data for GraphQL.
10199
*/
102100
export const graphqlIntegration = defineIntegration(_graphqlIntegration);
101+
102+
function getOptionsWithDefaults(options?: GraphqlOptions): GraphqlOptions {
103+
return {
104+
ignoreResolveSpans: true,
105+
ignoreTrivialResolveSpans: true,
106+
useOperationNameForRootSpan: true,
107+
...options,
108+
};
109+
}

packages/node/src/otel/instrument.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { Instrumentation } from '@opentelemetry/instrumentation';
22
import { addOpenTelemetryInstrumentation } from '@sentry/opentelemetry';
33

4-
const INSTRUMENTED: Record<string, Instrumentation> = {};
4+
/** Exported only for tests. */
5+
export const INSTRUMENTED: Record<string, Instrumentation> = {};
56

67
/**
78
* Instrument an OpenTelemetry instrumentation once.

0 commit comments

Comments
 (0)