Skip to content

Commit 3db1547

Browse files
authored
Merge pull request #8560 from getsentry/prepare-release/7.59.0
2 parents 338010e + 5eb0bdd commit 3db1547

File tree

45 files changed

+1000
-112
lines changed

Some content is hidden

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

45 files changed

+1000
-112
lines changed

CHANGELOG.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,60 @@
44

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

7+
## 7.59.0
8+
9+
### Important Changes
10+
11+
- **- feat(remix): Add Remix v2 support (#8415)**
12+
13+
This release adds support for Remix v2 future flags, in particular for new error handling utilities of Remix v2. We heavily recommend you switch to using `v2_errorBoundary` future flag to get the best error handling experience with Sentry.
14+
15+
To capture errors from [v2 client-side ErrorBoundary](https://remix.run/docs/en/main/route/error-boundary-v2), you should define your own `ErrorBoundary` in `root.tsx` and use `Sentry.captureRemixErrorBoundaryError` helper to capture the error.
16+
17+
```typescript
18+
// root.tsx
19+
import { captureRemixErrorBoundaryError } from "@sentry/remix";
20+
21+
export const ErrorBoundary: V2_ErrorBoundaryComponent = () => {
22+
const error = useRouteError();
23+
24+
captureRemixErrorBoundaryError(error);
25+
26+
return <div> ... </div>;
27+
};
28+
```
29+
30+
For server-side errors, define a [`handleError`](https://remix.run/docs/en/main/file-conventions/entry.server#handleerror) function in your server entry point and use the `Sentry.captureRemixServerException` helper to capture the error.
31+
32+
```ts
33+
// entry.server.tsx
34+
export function handleError(
35+
error: unknown,
36+
{ request }: DataFunctionArgs
37+
): void {
38+
if (error instanceof Error) {
39+
Sentry.captureRemixServerException(error, "remix.server", request);
40+
} else {
41+
// Optionally capture non-Error objects
42+
Sentry.captureException(error);
43+
}
44+
}
45+
```
46+
47+
For more details, see the Sentry [Remix SDK](https://docs.sentry.io/platforms/javascript/guides/remix/) documentation.
48+
49+
### Other Changes
50+
51+
- feat(core): Add `ModuleMetadata` integration (#8475)
52+
- feat(core): Allow multiplexed transport to send to multiple releases (#8559)
53+
- feat(tracing): Add more network timings to http calls (#8540)
54+
- feat(tracing): Bring http timings out of experiment (#8563)
55+
- fix(nextjs): Avoid importing `SentryWebpackPlugin` in dev mode (#8557)
56+
- fix(otel): Use `HTTP_URL` attribute for client requests (#8539)
57+
- fix(replay): Better session storage check (#8547)
58+
- fix(replay): Handle errors in `beforeAddRecordingEvent` callback (#8548)
59+
- fix(tracing): Improve network.protocol.version (#8502)
60+
761
## 7.58.1
862

963
- fix(node): Set propagation context even when tracingOptions are not defined (#8517)

packages/browser-integration-tests/suites/tracing/browsertracing/http-timings/test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,16 @@ sentryTest('should create fetch spans with http timing', async ({ browserName, g
4040
timestamp: expect.any(Number),
4141
trace_id: tracingEvent.contexts?.trace?.trace_id,
4242
data: expect.objectContaining({
43+
'http.request.redirect_start': expect.any(Number),
44+
'http.request.fetch_start': expect.any(Number),
45+
'http.request.domain_lookup_start': expect.any(Number),
46+
'http.request.domain_lookup_end': expect.any(Number),
4347
'http.request.connect_start': expect.any(Number),
48+
'http.request.secure_connection_start': expect.any(Number),
49+
'http.request.connection_end': expect.any(Number),
4450
'http.request.request_start': expect.any(Number),
4551
'http.request.response_start': expect.any(Number),
52+
'http.request.response_end': expect.any(Number),
4653
'network.protocol.version': expect.any(String),
4754
}),
4855
}),

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"build:types:core": "tsc -p tsconfig.types.json",
6565
"build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8",
6666
"build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch",
67-
"build:dev:watch": "yarn build:watch",
67+
"build:dev:watch": "run-p build:transpile:watch build:types:watch",
6868
"build:bundle:watch": "rollup -c rollup.bundle.config.js --watch",
6969
"build:transpile:watch": "rollup -c rollup.npm.config.js --watch",
7070
"build:types:watch": "tsc -p tsconfig.types.json --watch",

packages/browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export {
3434
spanStatusfromHttpCode,
3535
trace,
3636
makeMultiplexedTransport,
37+
ModuleMetadata,
3738
} from '@sentry/core';
3839
export type { SpanStatusType } from '@sentry/core';
3940
export type { Span } from '@sentry/types';

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export { prepareEvent } from './utils/prepareEvent';
4646
export { createCheckInEnvelope } from './checkin';
4747
export { hasTracingEnabled } from './utils/hasTracingEnabled';
4848
export { DEFAULT_ENVIRONMENT } from './constants';
49-
49+
export { ModuleMetadata } from './integrations/metadata';
5050
import * as Integrations from './integrations';
5151

5252
export { Integrations };
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { EventItem, EventProcessor, Hub, Integration } from '@sentry/types';
2+
import { forEachEnvelopeItem } from '@sentry/utils';
3+
4+
import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata';
5+
6+
/**
7+
* Adds module metadata to stack frames.
8+
*
9+
* Metadata can be injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
10+
*
11+
* When this integration is added, the metadata passed to the bundler plugin is added to the stack frames of all events
12+
* under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams
13+
* our sources
14+
*/
15+
export class ModuleMetadata implements Integration {
16+
/*
17+
* @inheritDoc
18+
*/
19+
public static id: string = 'ModuleMetadata';
20+
21+
/**
22+
* @inheritDoc
23+
*/
24+
public name: string = ModuleMetadata.id;
25+
26+
/**
27+
* @inheritDoc
28+
*/
29+
public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void {
30+
const client = getCurrentHub().getClient();
31+
32+
if (!client || typeof client.on !== 'function') {
33+
return;
34+
}
35+
36+
// We need to strip metadata from stack frames before sending them to Sentry since these are client side only.
37+
client.on('beforeEnvelope', envelope => {
38+
forEachEnvelopeItem(envelope, (item, type) => {
39+
if (type === 'event') {
40+
const event = Array.isArray(item) ? (item as EventItem)[1] : undefined;
41+
42+
if (event) {
43+
stripMetadataFromStackFrames(event);
44+
item[1] = event;
45+
}
46+
}
47+
});
48+
});
49+
50+
const stackParser = client.getOptions().stackParser;
51+
52+
addGlobalEventProcessor(event => {
53+
addMetadataToStackFrames(stackParser, event);
54+
return event;
55+
});
56+
}
57+
}

packages/core/src/transports/multiplexed.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ interface MatchParam {
2424
getEvent(types?: EnvelopeItemType[]): Event | undefined;
2525
}
2626

27-
type Matcher = (param: MatchParam) => string[];
27+
type RouteTo = { dsn: string; release: string };
28+
type Matcher = (param: MatchParam) => (string | RouteTo)[];
2829

29-
function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined {
30+
/**
31+
* Gets an event from an envelope.
32+
*
33+
* This is only exported for use in the tests
34+
*/
35+
export function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined {
3036
let event: Event | undefined;
3137

3238
forEachEnvelopeItem(env, (item, type) => {
@@ -40,6 +46,30 @@ function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | un
4046
return event;
4147
}
4248

49+
/**
50+
* Creates a transport that overrides the release on all events.
51+
*/
52+
function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
53+
createTransport: (options: TO) => Transport,
54+
release: string,
55+
): (options: TO) => Transport {
56+
return options => {
57+
const transport = createTransport(options);
58+
59+
return {
60+
send: async (envelope: Envelope): Promise<void | TransportMakeRequestResponse> => {
61+
const event = eventFromEnvelope(envelope, ['event', 'transaction', 'profile', 'replay_event']);
62+
63+
if (event) {
64+
event.release = release;
65+
}
66+
return transport.send(envelope);
67+
},
68+
flush: timeout => transport.flush(timeout),
69+
};
70+
};
71+
}
72+
4373
/**
4474
* Creates a transport that can send events to different DSNs depending on the envelope contents.
4575
*/
@@ -51,17 +81,24 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
5181
const fallbackTransport = createTransport(options);
5282
const otherTransports: Record<string, Transport> = {};
5383

54-
function getTransport(dsn: string): Transport | undefined {
55-
if (!otherTransports[dsn]) {
84+
function getTransport(dsn: string, release: string | undefined): Transport | undefined {
85+
// We create a transport for every unique dsn/release combination as there may be code from multiple releases in
86+
// use at the same time
87+
const key = release ? `${dsn}:${release}` : dsn;
88+
89+
if (!otherTransports[key]) {
5690
const validatedDsn = dsnFromString(dsn);
5791
if (!validatedDsn) {
5892
return undefined;
5993
}
6094
const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn);
61-
otherTransports[dsn] = createTransport({ ...options, url });
95+
96+
otherTransports[key] = release
97+
? makeOverrideReleaseTransport(createTransport, release)({ ...options, url })
98+
: createTransport({ ...options, url });
6299
}
63100

64-
return otherTransports[dsn];
101+
return otherTransports[key];
65102
}
66103

67104
async function send(envelope: Envelope): Promise<void | TransportMakeRequestResponse> {
@@ -71,7 +108,13 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
71108
}
72109

73110
const transports = matcher({ envelope, getEvent })
74-
.map(dsn => getTransport(dsn))
111+
.map(result => {
112+
if (typeof result === 'string') {
113+
return getTransport(result, undefined);
114+
} else {
115+
return getTransport(result.dsn, result.release);
116+
}
117+
})
75118
.filter((t): t is Transport => !!t);
76119

77120
// If we have no transports to send to, use the fallback transport
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Event } from '@sentry/types';
2+
import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, parseEnvelope } from '@sentry/utils';
3+
import { TextDecoder, TextEncoder } from 'util';
4+
5+
import { createTransport, getCurrentHub, ModuleMetadata } from '../../../src';
6+
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
7+
8+
const stackParser = createStackParser(nodeStackLineParser());
9+
10+
const stack = new Error().stack || '';
11+
12+
describe('ModuleMetadata integration', () => {
13+
beforeEach(() => {
14+
TestClient.sendEventCalled = undefined;
15+
TestClient.instance = undefined;
16+
17+
GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {};
18+
GLOBAL_OBJ._sentryModuleMetadata[stack] = { team: 'frontend' };
19+
});
20+
21+
afterEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
test('Adds and removes metadata from stack frames', done => {
26+
const options = getDefaultTestClientOptions({
27+
dsn: 'https://username@domain/123',
28+
enableSend: true,
29+
stackParser,
30+
integrations: [new ModuleMetadata()],
31+
beforeSend: (event, _hint) => {
32+
// copy the frames since reverse in in-place
33+
const lastFrame = [...(event.exception?.values?.[0].stacktrace?.frames || [])].reverse()[0];
34+
// Ensure module_metadata is populated in beforeSend callback
35+
expect(lastFrame?.module_metadata).toEqual({ team: 'frontend' });
36+
return event;
37+
},
38+
transport: () =>
39+
createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, async req => {
40+
const [, items] = parseEnvelope(req.body, new TextEncoder(), new TextDecoder());
41+
42+
expect(items[0][1]).toBeDefined();
43+
const event = items[0][1] as Event;
44+
const error = event.exception?.values?.[0];
45+
46+
// Ensure we're looking at the same error we threw
47+
expect(error?.value).toEqual('Some error');
48+
49+
const lastFrame = [...(error?.stacktrace?.frames || [])].reverse()[0];
50+
// Ensure the last frame is in fact for this file
51+
expect(lastFrame?.filename).toEqual(__filename);
52+
53+
// Ensure module_metadata has been stripped from the event
54+
expect(lastFrame?.module_metadata).toBeUndefined();
55+
56+
done();
57+
return {};
58+
}),
59+
});
60+
61+
const client = new TestClient(options);
62+
const hub = getCurrentHub();
63+
hub.bindClient(client);
64+
hub.captureException(new Error('Some error'));
65+
});
66+
});

packages/core/test/lib/transports/multiplexed.test.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import type {
66
TransactionEvent,
77
Transport,
88
} from '@sentry/types';
9-
import { createClientReportEnvelope, createEnvelope, dsnFromString } from '@sentry/utils';
10-
import { TextEncoder } from 'util';
9+
import { createClientReportEnvelope, createEnvelope, dsnFromString, parseEnvelope } from '@sentry/utils';
10+
import { TextDecoder, TextEncoder } from 'util';
1111

1212
import { createTransport, getEnvelopeEndpointWithUrlEncodedAuth, makeMultiplexedTransport } from '../../../src';
13+
import { eventFromEnvelope } from '../../../src/transports/multiplexed';
1314

1415
const DSN1 = 'https://[email protected]/4321';
1516
const DSN1_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN1)!);
@@ -47,7 +48,7 @@ const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope(
4748
123456,
4849
);
4950

50-
type Assertion = (url: string, body: string | Uint8Array) => void;
51+
type Assertion = (url: string, release: string | undefined, body: string | Uint8Array) => void;
5152

5253
const createTestTransport = (...assertions: Assertion[]): ((options: BaseTransportOptions) => Transport) => {
5354
return (options: BaseTransportOptions) =>
@@ -57,7 +58,10 @@ const createTestTransport = (...assertions: Assertion[]): ((options: BaseTranspo
5758
if (!assertion) {
5859
throw new Error('No assertion left');
5960
}
60-
assertion(options.url, request.body);
61+
62+
const event = eventFromEnvelope(parseEnvelope(request.body, new TextEncoder(), new TextDecoder()), ['event']);
63+
64+
assertion(options.url, event?.release, request.body);
6165
resolve({ statusCode: 200 });
6266
});
6367
});
@@ -111,6 +115,21 @@ describe('makeMultiplexedTransport', () => {
111115
await transport.send(ERROR_ENVELOPE);
112116
});
113117

118+
it('DSN and release can be overridden via match callback', async () => {
119+
expect.assertions(2);
120+
121+
const makeTransport = makeMultiplexedTransport(
122+
createTestTransport((url, release) => {
123+
expect(url).toBe(DSN2_URL);
124+
expect(release).toBe('[email protected]');
125+
}),
126+
() => [{ dsn: DSN2, release: '[email protected]' }],
127+
);
128+
129+
const transport = makeTransport({ url: DSN1_URL, ...transportOptions });
130+
await transport.send(ERROR_ENVELOPE);
131+
});
132+
114133
it('match callback can return multiple DSNs', async () => {
115134
expect.assertions(2);
116135

0 commit comments

Comments
 (0)