Skip to content

Commit

Permalink
Merge pull request #8560 from getsentry/prepare-release/7.59.0
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad authored Jul 17, 2023
2 parents 338010e + 5eb0bdd commit 3db1547
Show file tree
Hide file tree
Showing 45 changed files with 1,000 additions and 112 deletions.
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://1234@5678.ingest.sentry.io/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('something@1.0.0');
}),
() => [{ dsn: DSN2, release: 'something@1.0.0' }],
);

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

0 comments on commit 3db1547

Please sign in to comment.