Skip to content

meta: Update Changelog for 7.59.0 #8560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,60 @@

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

## 7.59.0

### Important Changes

- **- feat(remix): Add Remix v2 support (#8415)**

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.

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.

```typescript
// root.tsx
import { captureRemixErrorBoundaryError } from "@sentry/remix";

export const ErrorBoundary: V2_ErrorBoundaryComponent = () => {
const error = useRouteError();

captureRemixErrorBoundaryError(error);

return <div> ... </div>;
};
```

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.

```ts
// entry.server.tsx
export function handleError(
error: unknown,
{ request }: DataFunctionArgs
): void {
if (error instanceof Error) {
Sentry.captureRemixServerException(error, "remix.server", request);
} else {
// Optionally capture non-Error objects
Sentry.captureException(error);
}
}
```

For more details, see the Sentry [Remix SDK](https://docs.sentry.io/platforms/javascript/guides/remix/) documentation.

### Other Changes

- feat(core): Add `ModuleMetadata` integration (#8475)
- feat(core): Allow multiplexed transport to send to multiple releases (#8559)
- feat(tracing): Add more network timings to http calls (#8540)
- feat(tracing): Bring http timings out of experiment (#8563)
- fix(nextjs): Avoid importing `SentryWebpackPlugin` in dev mode (#8557)
- fix(otel): Use `HTTP_URL` attribute for client requests (#8539)
- fix(replay): Better session storage check (#8547)
- fix(replay): Handle errors in `beforeAddRecordingEvent` callback (#8548)
- fix(tracing): Improve network.protocol.version (#8502)

## 7.58.1

- fix(node): Set propagation context even when tracingOptions are not defined (#8517)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,16 @@ sentryTest('should create fetch spans with http timing', async ({ browserName, g
timestamp: expect.any(Number),
trace_id: tracingEvent.contexts?.trace?.trace_id,
data: expect.objectContaining({
'http.request.redirect_start': expect.any(Number),
'http.request.fetch_start': expect.any(Number),
'http.request.domain_lookup_start': expect.any(Number),
'http.request.domain_lookup_end': expect.any(Number),
'http.request.connect_start': expect.any(Number),
'http.request.secure_connection_start': expect.any(Number),
'http.request.connection_end': expect.any(Number),
'http.request.request_start': expect.any(Number),
'http.request.response_start': expect.any(Number),
'http.request.response_end': expect.any(Number),
'network.protocol.version': expect.any(String),
}),
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"build:types:core": "tsc -p tsconfig.types.json",
"build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8",
"build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch",
"build:dev:watch": "yarn build:watch",
"build:dev:watch": "run-p build:transpile:watch build:types:watch",
"build:bundle:watch": "rollup -c rollup.bundle.config.js --watch",
"build:transpile:watch": "rollup -c rollup.npm.config.js --watch",
"build:types:watch": "tsc -p tsconfig.types.json --watch",
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export {
spanStatusfromHttpCode,
trace,
makeMultiplexedTransport,
ModuleMetadata,
} from '@sentry/core';
export type { SpanStatusType } from '@sentry/core';
export type { Span } from '@sentry/types';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export { prepareEvent } from './utils/prepareEvent';
export { createCheckInEnvelope } from './checkin';
export { hasTracingEnabled } from './utils/hasTracingEnabled';
export { DEFAULT_ENVIRONMENT } from './constants';

export { ModuleMetadata } from './integrations/metadata';
import * as Integrations from './integrations';

export { Integrations };
57 changes: 57 additions & 0 deletions packages/core/src/integrations/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { EventItem, EventProcessor, Hub, Integration } from '@sentry/types';
import { forEachEnvelopeItem } from '@sentry/utils';

import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata';

/**
* Adds module metadata to stack frames.
*
* Metadata can be injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
*
* When this integration is added, the metadata passed to the bundler plugin is added to the stack frames of all events
* under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams
* our sources
*/
export class ModuleMetadata implements Integration {
/*
* @inheritDoc
*/
public static id: string = 'ModuleMetadata';

/**
* @inheritDoc
*/
public name: string = ModuleMetadata.id;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void {
const client = getCurrentHub().getClient();

if (!client || typeof client.on !== 'function') {
return;
}

// We need to strip metadata from stack frames before sending them to Sentry since these are client side only.
client.on('beforeEnvelope', envelope => {
forEachEnvelopeItem(envelope, (item, type) => {
if (type === 'event') {
const event = Array.isArray(item) ? (item as EventItem)[1] : undefined;

if (event) {
stripMetadataFromStackFrames(event);
item[1] = event;
}
}
});
});

const stackParser = client.getOptions().stackParser;

addGlobalEventProcessor(event => {
addMetadataToStackFrames(stackParser, event);
return event;
});
}
}
57 changes: 50 additions & 7 deletions packages/core/src/transports/multiplexed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ interface MatchParam {
getEvent(types?: EnvelopeItemType[]): Event | undefined;
}

type Matcher = (param: MatchParam) => string[];
type RouteTo = { dsn: string; release: string };
type Matcher = (param: MatchParam) => (string | RouteTo)[];

function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined {
/**
* Gets an event from an envelope.
*
* This is only exported for use in the tests
*/
export function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined {
let event: Event | undefined;

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

/**
* Creates a transport that overrides the release on all events.
*/
function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
createTransport: (options: TO) => Transport,
release: string,
): (options: TO) => Transport {
return options => {
const transport = createTransport(options);

return {
send: async (envelope: Envelope): Promise<void | TransportMakeRequestResponse> => {
const event = eventFromEnvelope(envelope, ['event', 'transaction', 'profile', 'replay_event']);

if (event) {
event.release = release;
}
return transport.send(envelope);
},
flush: timeout => transport.flush(timeout),
};
};
}

/**
* Creates a transport that can send events to different DSNs depending on the envelope contents.
*/
Expand All @@ -51,17 +81,24 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
const fallbackTransport = createTransport(options);
const otherTransports: Record<string, Transport> = {};

function getTransport(dsn: string): Transport | undefined {
if (!otherTransports[dsn]) {
function getTransport(dsn: string, release: string | undefined): Transport | undefined {
// We create a transport for every unique dsn/release combination as there may be code from multiple releases in
// use at the same time
const key = release ? `${dsn}:${release}` : dsn;

if (!otherTransports[key]) {
const validatedDsn = dsnFromString(dsn);
if (!validatedDsn) {
return undefined;
}
const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn);
otherTransports[dsn] = createTransport({ ...options, url });

otherTransports[key] = release
? makeOverrideReleaseTransport(createTransport, release)({ ...options, url })
: createTransport({ ...options, url });
}

return otherTransports[dsn];
return otherTransports[key];
}

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

const transports = matcher({ envelope, getEvent })
.map(dsn => getTransport(dsn))
.map(result => {
if (typeof result === 'string') {
return getTransport(result, undefined);
} else {
return getTransport(result.dsn, result.release);
}
})
.filter((t): t is Transport => !!t);

// If we have no transports to send to, use the fallback transport
Expand Down
66 changes: 66 additions & 0 deletions packages/core/test/lib/integrations/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Event } from '@sentry/types';
import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, parseEnvelope } from '@sentry/utils';
import { TextDecoder, TextEncoder } from 'util';

import { createTransport, getCurrentHub, ModuleMetadata } from '../../../src';
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';

const stackParser = createStackParser(nodeStackLineParser());

const stack = new Error().stack || '';

describe('ModuleMetadata integration', () => {
beforeEach(() => {
TestClient.sendEventCalled = undefined;
TestClient.instance = undefined;

GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {};
GLOBAL_OBJ._sentryModuleMetadata[stack] = { team: 'frontend' };
});

afterEach(() => {
jest.clearAllMocks();
});

test('Adds and removes metadata from stack frames', done => {
const options = getDefaultTestClientOptions({
dsn: 'https://username@domain/123',
enableSend: true,
stackParser,
integrations: [new ModuleMetadata()],
beforeSend: (event, _hint) => {
// copy the frames since reverse in in-place
const lastFrame = [...(event.exception?.values?.[0].stacktrace?.frames || [])].reverse()[0];
// Ensure module_metadata is populated in beforeSend callback
expect(lastFrame?.module_metadata).toEqual({ team: 'frontend' });
return event;
},
transport: () =>
createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, async req => {
const [, items] = parseEnvelope(req.body, new TextEncoder(), new TextDecoder());

expect(items[0][1]).toBeDefined();
const event = items[0][1] as Event;
const error = event.exception?.values?.[0];

// Ensure we're looking at the same error we threw
expect(error?.value).toEqual('Some error');

const lastFrame = [...(error?.stacktrace?.frames || [])].reverse()[0];
// Ensure the last frame is in fact for this file
expect(lastFrame?.filename).toEqual(__filename);

// Ensure module_metadata has been stripped from the event
expect(lastFrame?.module_metadata).toBeUndefined();

done();
return {};
}),
});

const client = new TestClient(options);
const hub = getCurrentHub();
hub.bindClient(client);
hub.captureException(new Error('Some error'));
});
});
27 changes: 23 additions & 4 deletions packages/core/test/lib/transports/multiplexed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import type {
TransactionEvent,
Transport,
} from '@sentry/types';
import { createClientReportEnvelope, createEnvelope, dsnFromString } from '@sentry/utils';
import { TextEncoder } from 'util';
import { createClientReportEnvelope, createEnvelope, dsnFromString, parseEnvelope } from '@sentry/utils';
import { TextDecoder, TextEncoder } from 'util';

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

const DSN1 = 'https://[email protected]/4321';
const DSN1_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN1)!);
Expand Down Expand Up @@ -47,7 +48,7 @@ const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope(
123456,
);

type Assertion = (url: string, body: string | Uint8Array) => void;
type Assertion = (url: string, release: string | undefined, body: string | Uint8Array) => void;

const createTestTransport = (...assertions: Assertion[]): ((options: BaseTransportOptions) => Transport) => {
return (options: BaseTransportOptions) =>
Expand All @@ -57,7 +58,10 @@ const createTestTransport = (...assertions: Assertion[]): ((options: BaseTranspo
if (!assertion) {
throw new Error('No assertion left');
}
assertion(options.url, request.body);

const event = eventFromEnvelope(parseEnvelope(request.body, new TextEncoder(), new TextDecoder()), ['event']);

assertion(options.url, event?.release, request.body);
resolve({ statusCode: 200 });
});
});
Expand Down Expand Up @@ -111,6 +115,21 @@ describe('makeMultiplexedTransport', () => {
await transport.send(ERROR_ENVELOPE);
});

it('DSN and release can be overridden via match callback', async () => {
expect.assertions(2);

const makeTransport = makeMultiplexedTransport(
createTestTransport((url, release) => {
expect(url).toBe(DSN2_URL);
expect(release).toBe('[email protected]');
}),
() => [{ dsn: DSN2, release: '[email protected]' }],
);

const transport = makeTransport({ url: DSN1_URL, ...transportOptions });
await transport.send(ERROR_ENVELOPE);
});

it('match callback can return multiple DSNs', async () => {
expect.assertions(2);

Expand Down
Loading