Skip to content

Commit 89b00f6

Browse files
authored
Merge pull request #16480 from getsentry/prepare-release/9.26.0
meta(changelog): Update changelog for 9.26.0
2 parents f43737f + f2e28a3 commit 89b00f6

File tree

8 files changed

+573
-144
lines changed

8 files changed

+573
-144
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
6+
7+
## 9.26.0
8+
9+
- feat(react-router): Re-export functions from `@sentry/react` ([#16465](https://github.com/getsentry/sentry-javascript/pull/16465))
10+
- fix(nextjs): Skip re instrumentating on generate phase of experimental build mode ([#16410](https://github.com/getsentry/sentry-javascript/pull/16410))
11+
- fix(node): Ensure adding sentry-trace and baggage headers via SentryHttpInstrumentation doesn't crash ([#16473](https://github.com/getsentry/sentry-javascript/pull/16473))
12+
313
## 9.25.1
414

515
- fix(otel): Don't ignore child spans after the root is sent ([#16416](https://github.com/getsentry/sentry-javascript/pull/16416))

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getNextjsVersion } from './util';
1515
import { constructWebpackConfigFunction } from './webpack';
1616

1717
let showedExportModeTunnelWarning = false;
18+
let showedExperimentalBuildModeWarning = false;
1819

1920
/**
2021
* Modifies the passed in Next.js configuration with automatic build-time instrumentation and source map upload.
@@ -67,6 +68,27 @@ function getFinalConfigObject(
6768
}
6869
}
6970

71+
if (process.argv.includes('--experimental-build-mode')) {
72+
if (!showedExperimentalBuildModeWarning) {
73+
showedExperimentalBuildModeWarning = true;
74+
// eslint-disable-next-line no-console
75+
console.warn(
76+
'[@sentry/nextjs] The Sentry Next.js SDK does not currently fully support next build --experimental-build-mode',
77+
);
78+
}
79+
if (process.argv.includes('generate')) {
80+
// Next.js v15.3.0-canary.1 splits the experimental build into two phases:
81+
// 1. compile: Code compilation
82+
// 2. generate: Environment variable inlining and prerendering (We don't instrument this phase, we inline in the compile phase)
83+
//
84+
// We assume a single “full” build and reruns Webpack instrumentation in both phases.
85+
// During the generate step it collides with Next.js’s inliner
86+
// producing malformed JS and build failures.
87+
// We skip Sentry processing during generate to avoid this issue.
88+
return incomingUserNextConfigObject;
89+
}
90+
}
91+
7092
setUpBuildTimeVariables(incomingUserNextConfigObject, userSentryOptions, releaseName);
7193

7294
const nextJsVersion = getNextjsVersion();

packages/nextjs/test/config/withSentryConfig.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,29 @@ describe('withSentryConfig', () => {
5050

5151
expect(exportedNextConfigFunction).toHaveBeenCalledWith(defaultRuntimePhase, defaultsObject);
5252
});
53+
54+
it('handles experimental build mode correctly', () => {
55+
const originalArgv = process.argv;
56+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
57+
58+
try {
59+
process.argv = [...originalArgv, '--experimental-build-mode'];
60+
materializeFinalNextConfig(exportedNextConfig);
61+
62+
expect(consoleWarnSpy).toHaveBeenCalledWith(
63+
'[@sentry/nextjs] The Sentry Next.js SDK does not currently fully support next build --experimental-build-mode',
64+
);
65+
66+
// Generate phase
67+
process.argv = [...process.argv, 'generate'];
68+
const generateConfig = materializeFinalNextConfig(exportedNextConfig);
69+
70+
expect(generateConfig).toEqual(exportedNextConfig);
71+
72+
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
73+
} finally {
74+
process.argv = originalArgv;
75+
consoleWarnSpy.mockRestore();
76+
}
77+
});
5378
});

packages/node/src/integrations/http/SentryHttpInstrumentation.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
getSanitizedUrlString,
2121
getTraceData,
2222
httpRequestToRequestData,
23+
isError,
2324
logger,
2425
LRUMap,
2526
parseUrl,
@@ -262,15 +263,34 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
262263

263264
// We do not want to overwrite existing header here, if it was already set
264265
if (sentryTrace && !request.getHeader('sentry-trace')) {
265-
request.setHeader('sentry-trace', sentryTrace);
266-
logger.log(INSTRUMENTATION_NAME, 'Added sentry-trace header to outgoing request');
266+
try {
267+
request.setHeader('sentry-trace', sentryTrace);
268+
DEBUG_BUILD && logger.log(INSTRUMENTATION_NAME, 'Added sentry-trace header to outgoing request');
269+
} catch (error) {
270+
DEBUG_BUILD &&
271+
logger.error(
272+
INSTRUMENTATION_NAME,
273+
'Failed to add sentry-trace header to outgoing request:',
274+
isError(error) ? error.message : 'Unknown error',
275+
);
276+
}
267277
}
268278

269279
if (baggage) {
270280
// For baggage, we make sure to merge this into a possibly existing header
271281
const newBaggage = mergeBaggageHeaders(request.getHeader('baggage'), baggage);
272282
if (newBaggage) {
273-
request.setHeader('baggage', newBaggage);
283+
try {
284+
request.setHeader('baggage', newBaggage);
285+
DEBUG_BUILD && logger.log(INSTRUMENTATION_NAME, 'Added baggage header to outgoing request');
286+
} catch (error) {
287+
DEBUG_BUILD &&
288+
logger.error(
289+
INSTRUMENTATION_NAME,
290+
'Failed to add baggage header to outgoing request:',
291+
isError(error) ? error.message : 'Unknown error',
292+
);
293+
}
274294
}
275295
}
276296
}

packages/react-router/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@
4242
"@sentry/cli": "^2.45.0",
4343
"@sentry/core": "9.25.1",
4444
"@sentry/node": "9.25.1",
45+
"@sentry/react": "9.25.1",
4546
"@sentry/vite-plugin": "^3.2.4",
4647
"glob": "11.0.1"
4748
},
4849
"devDependencies": {
49-
"@react-router/dev": "^7.1.5",
50-
"@react-router/node": "^7.1.5",
51-
"react-router": "^7.1.5",
50+
"@react-router/dev": "^7.5.2",
51+
"@react-router/node": "^7.5.2",
52+
"react-router": "^7.5.2",
5253
"vite": "^6.1.0"
5354
},
5455
"peerDependencies": {

packages/react-router/src/client/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,14 @@ export * from '@sentry/browser';
22

33
export { init } from './sdk';
44
export { reactRouterTracingIntegration } from './tracingIntegration';
5+
6+
export {
7+
captureReactException,
8+
reactErrorHandler,
9+
Profiler,
10+
withProfiler,
11+
useProfiler,
12+
ErrorBoundary,
13+
withErrorBoundary,
14+
} from '@sentry/react';
15+
export type { ErrorBoundaryProps, FallbackRender } from '@sentry/react';
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as SentryReact from '@sentry/react';
2+
import { render } from '@testing-library/react';
3+
import * as React from 'react';
4+
import { afterEach, describe, expect, it, vi } from 'vitest';
5+
import type { ErrorBoundaryProps, FallbackRender } from '../../src/client';
6+
import {
7+
captureReactException,
8+
ErrorBoundary,
9+
Profiler,
10+
reactErrorHandler,
11+
useProfiler,
12+
withErrorBoundary,
13+
withProfiler,
14+
} from '../../src/client';
15+
16+
describe('Re-exports from React SDK', () => {
17+
afterEach(() => {
18+
vi.clearAllMocks();
19+
});
20+
21+
describe('re-export integrity', () => {
22+
it('should have the same reference as original @sentry/react exports', () => {
23+
// Ensure we are re-exporting the exact same functions/components, not copies
24+
expect(captureReactException).toBe(SentryReact.captureReactException);
25+
expect(reactErrorHandler).toBe(SentryReact.reactErrorHandler);
26+
expect(Profiler).toBe(SentryReact.Profiler);
27+
expect(withProfiler).toBe(SentryReact.withProfiler);
28+
expect(useProfiler).toBe(SentryReact.useProfiler);
29+
expect(ErrorBoundary).toBe(SentryReact.ErrorBoundary);
30+
expect(withErrorBoundary).toBe(SentryReact.withErrorBoundary);
31+
});
32+
});
33+
34+
describe('function exports', () => {
35+
it('captureReactException should work when called', () => {
36+
const error = new Error('test error');
37+
const errorInfo = { componentStack: 'component stack' };
38+
39+
const result = captureReactException(error, errorInfo);
40+
expect(typeof result).toBe('string');
41+
expect(result).toMatch(/^[a-f0-9]{32}$/); // assuming event ID is a 32-character hex string
42+
});
43+
});
44+
45+
describe('component exports', () => {
46+
it('ErrorBoundary should render children when no error occurs', () => {
47+
const { getByText } = render(
48+
React.createElement(
49+
ErrorBoundary,
50+
{ fallback: () => React.createElement('div', null, 'Error occurred') },
51+
React.createElement('div', null, 'Child content'),
52+
),
53+
);
54+
55+
expect(getByText('Child content')).toBeDefined();
56+
});
57+
58+
it('Profiler should render children', () => {
59+
const { getByText } = render(
60+
React.createElement(
61+
Profiler,
62+
{ name: 'TestProfiler', updateProps: {} },
63+
React.createElement('div', null, 'Profiled content'),
64+
),
65+
);
66+
67+
expect(getByText('Profiled content')).toBeDefined();
68+
});
69+
});
70+
71+
describe('HOC exports', () => {
72+
it('withErrorBoundary should create a wrapped component', () => {
73+
const TestComponent = () => React.createElement('div', null, 'ErrorBoundary Test Component');
74+
const WrappedComponent = withErrorBoundary(TestComponent, {
75+
fallback: () => React.createElement('div', null, 'Error occurred'),
76+
});
77+
78+
expect(WrappedComponent).toBeDefined();
79+
expect(typeof WrappedComponent).toBe('function');
80+
expect(WrappedComponent.displayName).toBe('errorBoundary(TestComponent)');
81+
82+
const { getByText } = render(React.createElement(WrappedComponent));
83+
expect(getByText('ErrorBoundary Test Component')).toBeDefined();
84+
});
85+
86+
it('withProfiler should create a wrapped component', () => {
87+
const TestComponent = () => React.createElement('div', null, 'Profiler Test Component');
88+
const WrappedComponent = withProfiler(TestComponent, { name: 'TestComponent' });
89+
90+
expect(WrappedComponent).toBeDefined();
91+
expect(typeof WrappedComponent).toBe('function');
92+
expect(WrappedComponent.displayName).toBe('profiler(TestComponent)');
93+
94+
const { getByText } = render(React.createElement(WrappedComponent));
95+
expect(getByText('Profiler Test Component')).toBeDefined();
96+
});
97+
});
98+
99+
describe('type exports', () => {
100+
it('should export ErrorBoundaryProps type', () => {
101+
// This is a compile-time test - if this compiles, the type is exported correctly
102+
const props: ErrorBoundaryProps = {
103+
children: React.createElement('div'),
104+
fallback: () => React.createElement('div', null, 'Error'),
105+
};
106+
expect(props).toBeDefined();
107+
});
108+
109+
it('should export FallbackRender type', () => {
110+
// This is a compile-time test - if this compiles, the type is exported correctly
111+
const fallbackRender: FallbackRender = ({ error }) =>
112+
React.createElement('div', null, `Error: ${error?.toString()}`);
113+
expect(fallbackRender).toBeDefined();
114+
expect(typeof fallbackRender).toBe('function');
115+
});
116+
});
117+
});

0 commit comments

Comments
 (0)