Skip to content

Commit ac09686

Browse files
Lms24mydea
andauthored
Merge pull request #9817 from getsentry/prepare-release/7.87.0
Co-authored-by: Francesco Novy <[email protected]>
2 parents a063fbc + 28ba019 commit ac09686

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+531
-185
lines changed

.github/dependabot.yml

+11
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,14 @@ updates:
88
prefix: ci
99
prefix-development: ci
1010
include: scope
11+
- package-ecosystem: 'npm'
12+
directory: '/'
13+
schedule:
14+
interval: 'weekly'
15+
allow:
16+
- dependency-name: "@sentry/cli"
17+
- dependency-name: "@sentry/vite-plugin"
18+
commit-message:
19+
prefix: feat
20+
prefix-development: feat
21+
include: scope

.vscode/settings.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
],
3131
"deno.enablePaths": ["packages/deno/test"],
3232
"editor.codeActionsOnSave": {
33-
"source.organizeImports.biome": true,
34-
"quickfix.biome": true
33+
"source.organizeImports.biome": "explicit",
34+
"quickfix.biome": "explicit"
3535
},
3636
"editor.defaultFormatter": "biomejs.biome"
3737
}

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

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

7+
## 7.87.0
8+
9+
- feat: Add top level `getCurrentScope()` method (#9800)
10+
- feat(replay): Bump `rrweb` to 2.5.0 (#9803)
11+
- feat(replay): Capture hydration error breadcrumb (#9759)
12+
- feat(types): Add profile envelope types (#9798)
13+
- fix(astro): Avoid RegExp creation during route interpolation (#9815)
14+
- fix(browser): Avoid importing from `./exports` (#9775)
15+
- fix(nextjs): Catch rejecting flushes (#9811)
16+
- fix(nextjs): Fix devserver CORS blockage when `assetPrefix` is defined (#9766)
17+
- fix(node): Capture errors in tRPC middleware (#9782)
18+
719
## 7.86.0
820

921
- feat(core): Use SDK_VERSION for hub API version (#9732)

packages/astro/src/index.server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
getHubFromCarrier,
2626
getCurrentHub,
2727
getClient,
28+
getCurrentScope,
2829
Hub,
2930
makeMain,
3031
Scope,

packages/astro/src/server/middleware.ts

+80-14
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ import {
77
startSpan,
88
} from '@sentry/node';
99
import type { Hub, Span } from '@sentry/types';
10-
import {
11-
addNonEnumerableProperty,
12-
objectify,
13-
stripUrlQueryAndFragment,
14-
tracingContextFromHeaders,
15-
} from '@sentry/utils';
10+
import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils';
1611
import type { APIContext, MiddlewareResponseHandler } from 'astro';
1712

1813
import { getTracingMetaTags } from './meta';
@@ -64,7 +59,11 @@ type AstroLocalsWithSentry = Record<string, unknown> & {
6459
};
6560

6661
export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseHandler = options => {
67-
const handlerOptions = { trackClientIp: false, trackHeaders: false, ...options };
62+
const handlerOptions = {
63+
trackClientIp: false,
64+
trackHeaders: false,
65+
...options,
66+
};
6867

6968
return async (ctx, next) => {
7069
// if there is an active span, we know that this handle call is nested and hence
@@ -113,18 +112,19 @@ async function instrumentRequest(
113112
}
114113

115114
try {
115+
const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params);
116116
// storing res in a variable instead of directly returning is necessary to
117117
// invoke the catch block if next() throws
118118
const res = await startSpan(
119119
{
120120
...traceCtx,
121-
name: `${method} ${interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params)}`,
121+
name: `${method} ${interpolatedRoute || ctx.url.pathname}`,
122122
op: 'http.server',
123123
origin: 'auto.http.astro',
124124
status: 'ok',
125125
metadata: {
126126
...traceCtx?.metadata,
127-
source: 'route',
127+
source: interpolatedRoute ? 'route' : 'url',
128128
},
129129
data: {
130130
method,
@@ -202,10 +202,76 @@ function addMetaTagToHead(htmlChunk: string, hub: Hub, span?: Span): string {
202202
* Best we can do to get a route name instead of a raw URL.
203203
*
204204
* exported for testing
205+
*
206+
* @param rawUrlPathname - The raw URL pathname, e.g. '/users/123/details'
207+
* @param params - The params object, e.g. `{ userId: '123' }`
208+
*
209+
* @returns The interpolated route, e.g. '/users/[userId]/details'
205210
*/
206-
export function interpolateRouteFromUrlAndParams(rawUrl: string, params: APIContext['params']): string {
207-
return Object.entries(params).reduce((interpolateRoute, value) => {
208-
const [paramId, paramValue] = value;
209-
return interpolateRoute.replace(new RegExp(`(/|-)${paramValue}(/|-|$)`), `$1[${paramId}]$2`);
210-
}, rawUrl);
211+
export function interpolateRouteFromUrlAndParams(
212+
rawUrlPathname: string,
213+
params: APIContext['params'],
214+
): string | undefined {
215+
const decodedUrlPathname = tryDecodeUrl(rawUrlPathname);
216+
if (!decodedUrlPathname) {
217+
return undefined;
218+
}
219+
220+
// Invert params map so that the param values are the keys
221+
// differentiate between rest params spanning multiple url segments
222+
// and normal, single-segment params.
223+
const valuesToMultiSegmentParams: Record<string, string> = {};
224+
const valuesToParams: Record<string, string> = {};
225+
Object.entries(params).forEach(([key, value]) => {
226+
if (!value) {
227+
return;
228+
}
229+
if (value.includes('/')) {
230+
valuesToMultiSegmentParams[value] = key;
231+
return;
232+
}
233+
valuesToParams[value] = key;
234+
});
235+
236+
function replaceWithParamName(segment: string): string {
237+
const param = valuesToParams[segment];
238+
if (param) {
239+
return `[${param}]`;
240+
}
241+
return segment;
242+
}
243+
244+
// before we match single-segment params, we first replace multi-segment params
245+
const urlWithReplacedMultiSegmentParams = Object.keys(valuesToMultiSegmentParams).reduce((acc, key) => {
246+
return acc.replace(key, `[${valuesToMultiSegmentParams[key]}]`);
247+
}, decodedUrlPathname);
248+
249+
return urlWithReplacedMultiSegmentParams
250+
.split('/')
251+
.map(segment => {
252+
if (!segment) {
253+
return '';
254+
}
255+
256+
if (valuesToParams[segment]) {
257+
return replaceWithParamName(segment);
258+
}
259+
260+
// astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/
261+
const segmentParts = segment.split('-');
262+
if (segmentParts.length > 1) {
263+
return segmentParts.map(part => replaceWithParamName(part)).join('-');
264+
}
265+
266+
return segment;
267+
})
268+
.join('/');
269+
}
270+
271+
function tryDecodeUrl(url: string): string | undefined {
272+
try {
273+
return decodeURI(url);
274+
} catch {
275+
return undefined;
276+
}
211277
}

packages/astro/test/server/middleware.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,43 @@ describe('sentryMiddleware', () => {
6969
expect(resultFromNext).toStrictEqual(nextResult);
7070
});
7171

72+
it("sets source route if the url couldn't be decoded correctly", async () => {
73+
const middleware = handleRequest();
74+
const ctx = {
75+
request: {
76+
method: 'GET',
77+
url: '/a%xx',
78+
headers: new Headers(),
79+
},
80+
url: { pathname: 'a%xx', href: 'http://localhost:1234/a%xx' },
81+
params: {},
82+
};
83+
const next = vi.fn(() => nextResult);
84+
85+
// @ts-expect-error, a partial ctx object is fine here
86+
const resultFromNext = middleware(ctx, next);
87+
88+
expect(startSpanSpy).toHaveBeenCalledWith(
89+
{
90+
data: {
91+
method: 'GET',
92+
url: 'http://localhost:1234/a%xx',
93+
},
94+
metadata: {
95+
source: 'url',
96+
},
97+
name: 'GET a%xx',
98+
op: 'http.server',
99+
origin: 'auto.http.astro',
100+
status: 'ok',
101+
},
102+
expect.any(Function), // the `next` function
103+
);
104+
105+
expect(next).toHaveBeenCalled();
106+
expect(resultFromNext).toStrictEqual(nextResult);
107+
});
108+
72109
it('throws and sends an error to sentry if `next()` throws', async () => {
73110
const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException');
74111

@@ -299,15 +336,31 @@ describe('sentryMiddleware', () => {
299336

300337
describe('interpolateRouteFromUrlAndParams', () => {
301338
it.each([
339+
['/', {}, '/'],
302340
['/foo/bar', {}, '/foo/bar'],
303341
['/users/123', { id: '123' }, '/users/[id]'],
304342
['/users/123', { id: '123', foo: 'bar' }, '/users/[id]'],
305343
['/lang/en-US', { lang: 'en', region: 'US' }, '/lang/[lang]-[region]'],
306344
['/lang/en-US/posts', { lang: 'en', region: 'US' }, '/lang/[lang]-[region]/posts'],
345+
// edge cases that astro doesn't support
346+
['/lang/-US', { region: 'US' }, '/lang/-[region]'],
347+
['/lang/en-', { lang: 'en' }, '/lang/[lang]-'],
307348
])('interpolates route from URL and params %s', (rawUrl, params, expectedRoute) => {
308349
expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute);
309350
});
310351

352+
it.each([
353+
['/(a+)+/aaaaaaaaa!', { id: '(a+)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'],
354+
['/([a-zA-Z]+)*/aaaaaaaaa!', { id: '([a-zA-Z]+)*', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'],
355+
['/(a|aa)+/aaaaaaaaa!', { id: '(a|aa)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'],
356+
['/(a|a?)+/aaaaaaaaa!', { id: '(a|a?)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'],
357+
// with URL encoding
358+
['/(a%7Caa)+/aaaaaaaaa!', { id: '(a|aa)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'],
359+
['/(a%7Ca?)+/aaaaaaaaa!', { id: '(a|a?)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'],
360+
])('handles regex characters in param values correctly %s', (rawUrl, params, expectedRoute) => {
361+
expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute);
362+
});
363+
311364
it('handles params across multiple URL segments in catchall routes', () => {
312365
// Ideally, Astro would let us know that this is a catchall route so we can make the param [...catchall] but it doesn't
313366
expect(
@@ -324,4 +377,11 @@ describe('interpolateRouteFromUrlAndParams', () => {
324377
const expectedRoute = '/usernames/[name]';
325378
expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute);
326379
});
380+
381+
it('handles set but undefined params', () => {
382+
const rawUrl = '/usernames/user';
383+
const params = { name: undefined, name2: '' };
384+
const expectedRoute = '/usernames/user';
385+
expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute);
386+
});
327387
});

packages/browser/src/exports.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {
3636
getHubFromCarrier,
3737
getCurrentHub,
3838
getClient,
39+
getCurrentScope,
3940
Hub,
4041
lastEventId,
4142
makeMain,

packages/browser/src/integrations/breadcrumbs.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-lines */
2-
import { getCurrentHub } from '@sentry/core';
2+
import { getClient, getCurrentHub } from '@sentry/core';
33
import type {
44
Event as SentryEvent,
55
HandlerDataConsole,
@@ -31,7 +31,6 @@ import {
3131
} from '@sentry/utils';
3232

3333
import { DEBUG_BUILD } from '../debug-build';
34-
import { getClient } from '../exports';
3534
import { WINDOW } from '../helpers';
3635

3736
/** JSDoc */

packages/browser/src/profiling/integration.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
1+
import type { EventEnvelope, EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
22
import type { Profile } from '@sentry/types/src/profiling';
33
import { logger } from '@sentry/utils';
44

@@ -110,7 +110,7 @@ export class BrowserProfilingIntegration implements Integration {
110110
}
111111
}
112112

113-
addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
113+
addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope);
114114
});
115115
} else {
116116
logger.warn('[Profiling] Client does not support hooks, profiling will be disabled');

packages/browser/src/profiling/utils.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
/* eslint-disable max-lines */
22

3-
import { DEFAULT_ENVIRONMENT, getCurrentHub } from '@sentry/core';
4-
import type { DebugImage, Envelope, Event, StackFrame, StackParser, Transaction } from '@sentry/types';
3+
import { DEFAULT_ENVIRONMENT, getClient, getCurrentHub } from '@sentry/core';
4+
import type { DebugImage, Envelope, Event, EventEnvelope, StackFrame, StackParser, Transaction } from '@sentry/types';
55
import type { Profile, ThreadCpuProfile } from '@sentry/types/src/profiling';
66
import { GLOBAL_OBJ, browserPerformanceTimeOrigin, forEachEnvelopeItem, logger, uuid4 } from '@sentry/utils';
77

88
import { DEBUG_BUILD } from '../debug-build';
9-
import { getClient } from '../exports';
109
import { WINDOW } from '../helpers';
1110
import type { JSSelfProfile, JSSelfProfileStack, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling';
1211

@@ -301,13 +300,12 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Profi
301300
* Adds items to envelope if they are not already present - mutates the envelope.
302301
* @param envelope
303302
*/
304-
export function addProfilesToEnvelope(envelope: Envelope, profiles: Profile[]): Envelope {
303+
export function addProfilesToEnvelope(envelope: EventEnvelope, profiles: Profile[]): Envelope {
305304
if (!profiles.length) {
306305
return envelope;
307306
}
308307

309308
for (const profile of profiles) {
310-
// @ts-expect-error untyped envelope
311309
envelope[1].push([{ type: 'profile' }, profile]);
312310
}
313311
return envelope;

packages/bun/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export {
4242
getHubFromCarrier,
4343
getCurrentHub,
4444
getClient,
45+
getCurrentScope,
4546
Hub,
4647
lastEventId,
4748
makeMain,

packages/core/src/exports.ts

+7
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,10 @@ export function lastEventId(): string | undefined {
308308
export function getClient<C extends Client>(): C | undefined {
309309
return getCurrentHub().getClient<C>();
310310
}
311+
312+
/**
313+
* Get the currently active scope.
314+
*/
315+
export function getCurrentScope(): Scope {
316+
return getCurrentHub().getScope();
317+
}

packages/core/src/hub.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export class Hub implements HubInterface {
142142
*/
143143
public pushScope(): Scope {
144144
// We want to clone the content of prev scope
145-
const scope = Scope.clone(this.getScope());
145+
const scope = this.getScope().clone();
146146
this.getStack().push({
147147
client: this.getClient(),
148148
scope,
@@ -578,7 +578,7 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub(
578578
// If there's no hub on current domain, or it's an old API, assign a new one
579579
if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) {
580580
const globalHubTopStack = parent.getStackTop();
581-
setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, Scope.clone(globalHubTopStack.scope)));
581+
setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, globalHubTopStack.scope.clone()));
582582
}
583583
}
584584

packages/core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {
2626
setUser,
2727
withScope,
2828
getClient,
29+
getCurrentScope,
2930
} from './exports';
3031
export {
3132
getCurrentHub,

0 commit comments

Comments
 (0)