Skip to content

Commit f633beb

Browse files
authored
Merge pull request #15777 from getsentry/prepare-release/9.8.0
meta(changelog): Update changelog for 9.8.0
2 parents f2b861c + 6ce989c commit f633beb

16 files changed

+1637
-1140
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13+
## 9.8.0
14+
15+
- feat(node): Implement new continuous profiling API spec ([#15635](https://github.com/getsentry/sentry-javascript/pull/15635))
16+
- feat(profiling): Add platform to chunk envelope ([#15758](https://github.com/getsentry/sentry-javascript/pull/15758))
17+
- feat(react): Export captureReactException method ([#15746](https://github.com/getsentry/sentry-javascript/pull/15746))
18+
- fix(node): Check for `res.end` before passing to Proxy ([#15776](https://github.com/getsentry/sentry-javascript/pull/15776))
19+
- perf(core): Add short-circuits to `eventFilters` integration ([#15752](https://github.com/getsentry/sentry-javascript/pull/15752))
20+
- perf(node): Short circuit flushing on Vercel only for Vercel ([#15734](https://github.com/getsentry/sentry-javascript/pull/15734))
21+
1322
## 9.7.0
1423

1524
- feat(core): Add `captureLog` method ([#15717](https://github.com/getsentry/sentry-javascript/pull/15717))
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Continuous Profiling API Changes
2+
3+
The continuous profiling API has been redesigned to give developers more explicit control over profiling sessions while maintaining ease of use. This guide outlines the key changes.
4+
5+
## New Profiling Modes
6+
7+
### profileLifecycle Option
8+
9+
We've introduced a new `profileLifecycle` option that allows you to explicitly set how profiling sessions are managed:
10+
11+
- `manual` (default) - You control profiling sessions using the API methods
12+
- `trace` - Profiling sessions are automatically tied to traces
13+
14+
Previously, the profiling mode was implicitly determined by initialization options. Now you can clearly specify your intended behavior.
15+
16+
## New Sampling Controls
17+
18+
### profileSessionSampleRate
19+
20+
We've introduced `profileSessionSampleRate` to control what percentage of SDK instances will collect profiles. This is evaluated once during SDK initialization. This is particularly useful for:
21+
22+
- Controlling profiling costs across distributed services
23+
- Managing profiling in serverless environments where you may only want to profile a subset of instances
24+
25+
### Deprecations
26+
27+
The `profilesSampleRate` option has been deprecated in favor of the new sampling controls.
28+
The `profilesSampler` option hsa been deprecated in favor of manual profiler control.

packages/core/src/integrations/eventFilters.ts

+71-65
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,6 @@ export interface EventFiltersOptions {
3535

3636
const INTEGRATION_NAME = 'EventFilters';
3737

38-
const _eventFiltersIntegration = ((options: Partial<EventFiltersOptions> = {}) => {
39-
return {
40-
name: INTEGRATION_NAME,
41-
processEvent(event, _hint, client) {
42-
const clientOptions = client.getOptions();
43-
const mergedOptions = _mergeOptions(options, clientOptions);
44-
return _shouldDropEvent(event, mergedOptions) ? null : event;
45-
},
46-
};
47-
}) satisfies IntegrationFn;
48-
4938
/**
5039
* An integration that filters out events (errors and transactions) based on:
5140
*
@@ -59,7 +48,23 @@ const _eventFiltersIntegration = ((options: Partial<EventFiltersOptions> = {}) =
5948
*
6049
* Events filtered by this integration will not be sent to Sentry.
6150
*/
62-
export const eventFiltersIntegration = defineIntegration(_eventFiltersIntegration);
51+
export const eventFiltersIntegration = defineIntegration((options: Partial<EventFiltersOptions> = {}) => {
52+
let mergedOptions: Partial<EventFiltersOptions> | undefined;
53+
return {
54+
name: INTEGRATION_NAME,
55+
setup(client) {
56+
const clientOptions = client.getOptions();
57+
mergedOptions = _mergeOptions(options, clientOptions);
58+
},
59+
processEvent(event, _hint, client) {
60+
if (!mergedOptions) {
61+
const clientOptions = client.getOptions();
62+
mergedOptions = _mergeOptions(options, clientOptions);
63+
}
64+
return _shouldDropEvent(event, mergedOptions) ? null : event;
65+
},
66+
};
67+
});
6368

6469
/**
6570
* An integration that filters out events (errors and transactions) based on:
@@ -102,66 +107,72 @@ function _mergeOptions(
102107
}
103108

104109
function _shouldDropEvent(event: Event, options: Partial<EventFiltersOptions>): boolean {
105-
if (options.ignoreInternal && _isSentryError(event)) {
106-
DEBUG_BUILD &&
107-
logger.warn(`Event dropped due to being internal Sentry Error.\nEvent: ${getEventDescription(event)}`);
108-
return true;
109-
}
110-
if (_isIgnoredError(event, options.ignoreErrors)) {
111-
DEBUG_BUILD &&
112-
logger.warn(
113-
`Event dropped due to being matched by \`ignoreErrors\` option.\nEvent: ${getEventDescription(event)}`,
114-
);
115-
return true;
116-
}
117-
if (_isUselessError(event)) {
118-
DEBUG_BUILD &&
119-
logger.warn(
120-
`Event dropped due to not having an error message, error type or stacktrace.\nEvent: ${getEventDescription(
121-
event,
122-
)}`,
123-
);
124-
return true;
125-
}
126-
if (_isIgnoredTransaction(event, options.ignoreTransactions)) {
127-
DEBUG_BUILD &&
128-
logger.warn(
129-
`Event dropped due to being matched by \`ignoreTransactions\` option.\nEvent: ${getEventDescription(event)}`,
130-
);
131-
return true;
132-
}
133-
if (_isDeniedUrl(event, options.denyUrls)) {
134-
DEBUG_BUILD &&
135-
logger.warn(
136-
`Event dropped due to being matched by \`denyUrls\` option.\nEvent: ${getEventDescription(
137-
event,
138-
)}.\nUrl: ${_getEventFilterUrl(event)}`,
139-
);
140-
return true;
141-
}
142-
if (!_isAllowedUrl(event, options.allowUrls)) {
143-
DEBUG_BUILD &&
144-
logger.warn(
145-
`Event dropped due to not being matched by \`allowUrls\` option.\nEvent: ${getEventDescription(
146-
event,
147-
)}.\nUrl: ${_getEventFilterUrl(event)}`,
148-
);
149-
return true;
110+
if (!event.type) {
111+
// Filter errors
112+
113+
if (options.ignoreInternal && _isSentryError(event)) {
114+
DEBUG_BUILD &&
115+
logger.warn(`Event dropped due to being internal Sentry Error.\nEvent: ${getEventDescription(event)}`);
116+
return true;
117+
}
118+
if (_isIgnoredError(event, options.ignoreErrors)) {
119+
DEBUG_BUILD &&
120+
logger.warn(
121+
`Event dropped due to being matched by \`ignoreErrors\` option.\nEvent: ${getEventDescription(event)}`,
122+
);
123+
return true;
124+
}
125+
if (_isUselessError(event)) {
126+
DEBUG_BUILD &&
127+
logger.warn(
128+
`Event dropped due to not having an error message, error type or stacktrace.\nEvent: ${getEventDescription(
129+
event,
130+
)}`,
131+
);
132+
return true;
133+
}
134+
if (_isDeniedUrl(event, options.denyUrls)) {
135+
DEBUG_BUILD &&
136+
logger.warn(
137+
`Event dropped due to being matched by \`denyUrls\` option.\nEvent: ${getEventDescription(
138+
event,
139+
)}.\nUrl: ${_getEventFilterUrl(event)}`,
140+
);
141+
return true;
142+
}
143+
if (!_isAllowedUrl(event, options.allowUrls)) {
144+
DEBUG_BUILD &&
145+
logger.warn(
146+
`Event dropped due to not being matched by \`allowUrls\` option.\nEvent: ${getEventDescription(
147+
event,
148+
)}.\nUrl: ${_getEventFilterUrl(event)}`,
149+
);
150+
return true;
151+
}
152+
} else if (event.type === 'transaction') {
153+
// Filter transactions
154+
155+
if (_isIgnoredTransaction(event, options.ignoreTransactions)) {
156+
DEBUG_BUILD &&
157+
logger.warn(
158+
`Event dropped due to being matched by \`ignoreTransactions\` option.\nEvent: ${getEventDescription(event)}`,
159+
);
160+
return true;
161+
}
150162
}
151163
return false;
152164
}
153165

154166
function _isIgnoredError(event: Event, ignoreErrors?: Array<string | RegExp>): boolean {
155-
// If event.type, this is not an error
156-
if (event.type || !ignoreErrors || !ignoreErrors.length) {
167+
if (!ignoreErrors?.length) {
157168
return false;
158169
}
159170

160171
return getPossibleEventMessages(event).some(message => stringMatchesSomePattern(message, ignoreErrors));
161172
}
162173

163174
function _isIgnoredTransaction(event: Event, ignoreTransactions?: Array<string | RegExp>): boolean {
164-
if (event.type !== 'transaction' || !ignoreTransactions || !ignoreTransactions.length) {
175+
if (!ignoreTransactions?.length) {
165176
return false;
166177
}
167178

@@ -223,11 +234,6 @@ function _getEventFilterUrl(event: Event): string | null {
223234
}
224235

225236
function _isUselessError(event: Event): boolean {
226-
if (event.type) {
227-
// event is not an error
228-
return false;
229-
}
230-
231237
// We only want to consider events for dropping that actually have recorded exception values.
232238
if (!event.exception?.values?.length) {
233239
return false;

packages/core/src/types-hoist/profiling.ts

+7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ export interface ProfilingIntegration<T extends Client> extends Integration {
1414
}
1515

1616
export interface Profiler {
17+
/**
18+
* Starts the profiler.
19+
*/
1720
startProfiler(): void;
21+
22+
/**
23+
* Stops the profiler.
24+
*/
1825
stopProfiler(): void;
1926
}
2027

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

+56-41
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import { stealthWrap } from './utils';
99
type Http = typeof http;
1010
type Https = typeof https;
1111

12+
// The reason this "before OTEL" integration even exists is due to timing reasons. We need to be able to register a
13+
// `res.on('close')` handler **after** OTEL registers its own handler (which it uses to end spans), so that we can do
14+
// something (ie. flush) after OTEL has ended a span for a request. If you think about it like an onion:
15+
//
16+
// (Sentry after OTEL instrumentation
17+
// (OTEL instrumentation
18+
// (Sentry before OTEL instrumentation
19+
// (orig HTTP request handler))))
20+
//
21+
// registering an instrumentation before OTEL allows us to do this for incoming requests.
22+
1223
/**
1324
* A Sentry specific http instrumentation that is applied before the otel instrumentation.
1425
*/
@@ -70,46 +81,50 @@ export class SentryHttpInstrumentationBeforeOtel extends InstrumentationBase {
7081
function patchResponseToFlushOnServerlessPlatforms(res: http.OutgoingMessage): void {
7182
// Freely extend this function with other platforms if necessary
7283
if (process.env.VERCEL) {
73-
let markOnEndDone = (): void => undefined;
74-
const onEndDonePromise = new Promise<void>(res => {
75-
markOnEndDone = res;
76-
});
77-
78-
res.on('close', () => {
79-
markOnEndDone();
80-
});
81-
82-
// eslint-disable-next-line @typescript-eslint/unbound-method
83-
res.end = new Proxy(res.end, {
84-
apply(target, thisArg, argArray) {
85-
vercelWaitUntil(
86-
new Promise<void>(finishWaitUntil => {
87-
// Define a timeout that unblocks the lambda just to be safe so we're not indefinitely keeping it alive, exploding server bills
88-
const timeout = setTimeout(() => {
89-
finishWaitUntil();
90-
}, 2000);
91-
92-
onEndDonePromise
93-
.then(() => {
94-
DEBUG_BUILD && logger.log('Flushing events before Vercel Lambda freeze');
95-
return flush(2000);
96-
})
97-
.then(
98-
() => {
99-
clearTimeout(timeout);
100-
finishWaitUntil();
101-
},
102-
e => {
103-
clearTimeout(timeout);
104-
DEBUG_BUILD && logger.log('Error while flushing events for Vercel:\n', e);
105-
finishWaitUntil();
106-
},
107-
);
108-
}),
109-
);
110-
111-
return target.apply(thisArg, argArray);
112-
},
113-
});
84+
// In some cases res.end does not seem to be defined leading to errors if passed to Proxy
85+
// https://github.com/getsentry/sentry-javascript/issues/15759
86+
if (typeof res.end === 'function') {
87+
let markOnEndDone = (): void => undefined;
88+
const onEndDonePromise = new Promise<void>(res => {
89+
markOnEndDone = res;
90+
});
91+
92+
res.on('close', () => {
93+
markOnEndDone();
94+
});
95+
96+
// eslint-disable-next-line @typescript-eslint/unbound-method
97+
res.end = new Proxy(res.end, {
98+
apply(target, thisArg, argArray) {
99+
vercelWaitUntil(
100+
new Promise<void>(finishWaitUntil => {
101+
// Define a timeout that unblocks the lambda just to be safe so we're not indefinitely keeping it alive, exploding server bills
102+
const timeout = setTimeout(() => {
103+
finishWaitUntil();
104+
}, 2000);
105+
106+
onEndDonePromise
107+
.then(() => {
108+
DEBUG_BUILD && logger.log('Flushing events before Vercel Lambda freeze');
109+
return flush(2000);
110+
})
111+
.then(
112+
() => {
113+
clearTimeout(timeout);
114+
finishWaitUntil();
115+
},
116+
e => {
117+
clearTimeout(timeout);
118+
DEBUG_BUILD && logger.log('Error while flushing events for Vercel:\n', e);
119+
finishWaitUntil();
120+
},
121+
);
122+
}),
123+
);
124+
125+
return target.apply(thisArg, argArray);
126+
},
127+
});
128+
}
114129
}
115130
}

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,17 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
142142
return {
143143
name: INTEGRATION_NAME,
144144
setupOnce() {
145-
instrumentSentryHttpBeforeOtel();
145+
// Below, we instrument the Node.js HTTP API three times. 2 times Sentry-specific, 1 time OTEL specific.
146+
// Due to timing reasons, we sometimes need to apply Sentry instrumentation _before_ we apply the OTEL
147+
// instrumentation (e.g. to flush on serverless platforms), and sometimes we need to apply Sentry instrumentation
148+
// _after_ we apply OTEL instrumentation (e.g. for isolation scope handling and breadcrumbs).
149+
150+
// This is Sentry-specific instrumentation that is applied _before_ any OTEL instrumentation.
151+
if (process.env.VERCEL) {
152+
// Currently this instrumentation only does something when deployed on Vercel, so to save some overhead, we short circuit adding it here only for Vercel.
153+
// If it's functionality is extended in the future, feel free to remove the if statement and this comment.
154+
instrumentSentryHttpBeforeOtel();
155+
}
146156

147157
const instrumentSpans = _shouldInstrumentSpans(options, getClient<NodeClient>()?.getOptions());
148158

@@ -152,9 +162,7 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
152162
instrumentOtelHttp(instrumentationConfig);
153163
}
154164

155-
// This is the Sentry-specific instrumentation that isolates requests & creates breadcrumbs
156-
// Note that this _has_ to be wrapped after the OTEL instrumentation,
157-
// otherwise the isolation will not work correctly
165+
// This is Sentry-specific instrumentation that is applied _after_ any OTEL instrumentation.
158166
instrumentSentryHttp({
159167
...options,
160168
// If spans are not instrumented, it means the HttpInstrumentation has not been added

0 commit comments

Comments
 (0)