Skip to content

Commit b3269ca

Browse files
authored
Merge pull request #9849 from getsentry/prepare-release/7.88.0
meta(changelog): Update changelog for 7.88.0
2 parents 56845fb + 8b6af6f commit b3269ca

32 files changed

+13638
-234
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ jobs:
445445
- name: Set up Deno
446446
uses: denoland/[email protected]
447447
with:
448-
deno-version: v1.37.1
448+
deno-version: v1.38.5
449449
- name: Restore caches
450450
uses: ./.github/actions/restore-cache
451451
env:

CHANGELOG.md

+59
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,65 @@
44

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

7+
## 7.88.0
8+
9+
### Important Changes
10+
11+
- **feat(browser): Add browser metrics sdk (#9794)**
12+
13+
The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in the Browser SDKs (`@sentry/browser` and related framework SDKs). Via the newly introduced APIs, you can now flush metrics directly to Sentry.
14+
15+
To enable capturing metrics, you first need to add the `MetricsAggregator` integration.
16+
17+
```js
18+
Sentry.init({
19+
dsn: '__DSN__',
20+
integrations: [
21+
new Sentry.metrics.MetricsAggregator(),
22+
],
23+
});
24+
```
25+
26+
Then you'll be able to add `counters`, `sets`, `distributions`, and `gauges` under the `Sentry.metrics` namespace.
27+
28+
```js
29+
// Add 4 to a counter named `hits`
30+
Sentry.metrics.increment('hits', 4);
31+
32+
// Add 2 to gauge named `parallel_requests`, tagged with `happy: "no"`
33+
Sentry.metrics.gauge('parallel_requests', 2, { tags: { happy: 'no' } });
34+
35+
// Add 4.6 to a distribution named `response_time` with unit seconds
36+
Sentry.metrics.distribution('response_time', 4.6, { unit: 'seconds' });
37+
38+
// Add 2 to a set named `valuable.ids`
39+
Sentry.metrics.set('valuable.ids', 2);
40+
```
41+
42+
In a future release we'll add support for server runtimes (Node, Deno, Bun, Vercel Edge, etc.)
43+
44+
- **feat(deno): Optionally instrument `Deno.cron` (#9808)**
45+
46+
This releases add support for instrumenting [Deno cron's](https://deno.com/blog/cron) with [Sentry cron monitors](https://docs.sentry.io/product/crons/). This requires v1.38 of Deno run with the `--unstable` flag and the usage of the `DenoCron` Sentry integration.
47+
48+
```ts
49+
// Import from the Deno registry
50+
import * as Sentry from "https://deno.land/x/sentry/index.mjs";
51+
52+
Sentry.init({
53+
dsn: '__DSN__',
54+
integrations: [
55+
new Sentry.DenoCron(),
56+
],
57+
});
58+
```
59+
60+
### Other Changes
61+
62+
- feat(replay): Bump `rrweb` to 2.6.0 (#9847)
63+
- fix(nextjs): Guard against injecting multiple times (#9807)
64+
- ref(remix): Bump Sentry CLI to ^2.23.0 (#9773)
65+
766
## 7.87.0
867

968
- feat: Add top level `getCurrentScope()` method (#9800)

packages/browser/src/exports.ts

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export {
5757
withScope,
5858
FunctionToString,
5959
InboundFilters,
60+
metrics,
6061
} from '@sentry/core';
6162

6263
export { WINDOW } from './helpers';

packages/core/src/baseclient.ts

+29
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import type {
1616
FeedbackEvent,
1717
Integration,
1818
IntegrationClass,
19+
MetricBucketItem,
20+
MetricsAggregator,
1921
Outcome,
2022
PropagationContext,
2123
SdkMetadata,
@@ -49,6 +51,7 @@ import { createEventEnvelope, createSessionEnvelope } from './envelope';
4951
import { getCurrentHub } from './hub';
5052
import type { IntegrationIndex } from './integration';
5153
import { setupIntegration, setupIntegrations } from './integration';
54+
import { createMetricEnvelope } from './metrics/envelope';
5255
import type { Scope } from './scope';
5356
import { updateSession } from './session';
5457
import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext';
@@ -88,6 +91,13 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca
8891
* }
8992
*/
9093
export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
94+
/**
95+
* A reference to a metrics aggregator
96+
*
97+
* @experimental Note this is alpha API. It may experience breaking changes in the future.
98+
*/
99+
public metricsAggregator?: MetricsAggregator;
100+
91101
/** Options passed to the SDK. */
92102
protected readonly _options: O;
93103

@@ -264,6 +274,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
264274
public flush(timeout?: number): PromiseLike<boolean> {
265275
const transport = this._transport;
266276
if (transport) {
277+
if (this.metricsAggregator) {
278+
this.metricsAggregator.flush();
279+
}
267280
return this._isClientDoneProcessing(timeout).then(clientFinished => {
268281
return transport.flush(timeout).then(transportFlushed => clientFinished && transportFlushed);
269282
});
@@ -278,6 +291,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
278291
public close(timeout?: number): PromiseLike<boolean> {
279292
return this.flush(timeout).then(result => {
280293
this.getOptions().enabled = false;
294+
if (this.metricsAggregator) {
295+
this.metricsAggregator.close();
296+
}
281297
return result;
282298
});
283299
}
@@ -383,6 +399,19 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
383399
}
384400
}
385401

402+
/**
403+
* @inheritDoc
404+
*/
405+
public captureAggregateMetrics(metricBucketItems: Array<MetricBucketItem>): void {
406+
const metricsEnvelope = createMetricEnvelope(
407+
metricBucketItems,
408+
this._dsn,
409+
this._options._metadata,
410+
this._options.tunnel,
411+
);
412+
void this._sendEnvelope(metricsEnvelope);
413+
}
414+
386415
// Keep on() & emit() signatures in sync with types' client.ts interface
387416
/* eslint-disable @typescript-eslint/unified-signatures */
388417

packages/core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,6 @@ export { DEFAULT_ENVIRONMENT } from './constants';
6464
export { ModuleMetadata } from './integrations/metadata';
6565
export { RequestData } from './integrations/requestdata';
6666
import * as Integrations from './integrations';
67+
export { metrics } from './metrics/exports';
6768

6869
export { Integrations };

packages/core/src/integrations/metadata.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EventItem, EventProcessor, Hub, Integration } from '@sentry/types';
1+
import type { Client, Event, EventItem, EventProcessor, Hub, Integration } from '@sentry/types';
22
import { forEachEnvelopeItem } from '@sentry/utils';
33

44
import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata';
@@ -30,10 +30,13 @@ export class ModuleMetadata implements Integration {
3030
/**
3131
* @inheritDoc
3232
*/
33-
public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void {
34-
const client = getCurrentHub().getClient();
33+
public setupOnce(_addGlobalEventProcessor: (processor: EventProcessor) => void, _getCurrentHub: () => Hub): void {
34+
// noop
35+
}
3536

36-
if (!client || typeof client.on !== 'function') {
37+
/** @inheritDoc */
38+
public setup(client: Client): void {
39+
if (typeof client.on !== 'function') {
3740
return;
3841
}
3942

@@ -50,12 +53,12 @@ export class ModuleMetadata implements Integration {
5053
}
5154
});
5255
});
56+
}
5357

58+
/** @inheritDoc */
59+
public processEvent(event: Event, _hint: unknown, client: Client): Event {
5460
const stackParser = client.getOptions().stackParser;
55-
56-
addGlobalEventProcessor(event => {
57-
addMetadataToStackFrames(stackParser, event);
58-
return event;
59-
});
61+
addMetadataToStackFrames(stackParser, event);
62+
return event;
6063
}
6164
}

packages/core/src/integrations/requestdata.ts

+51-50
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Event, EventProcessor, Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types';
1+
import type { Client, Event, EventProcessor, Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types';
22
import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils';
33
import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils';
44

@@ -95,65 +95,66 @@ export class RequestData implements Integration {
9595
/**
9696
* @inheritDoc
9797
*/
98-
public setupOnce(addGlobalEventProcessor: (eventProcessor: EventProcessor) => void, getCurrentHub: () => Hub): void {
98+
public setupOnce(
99+
_addGlobalEventProcessor: (eventProcessor: EventProcessor) => void,
100+
_getCurrentHub: () => Hub,
101+
): void {
102+
// noop
103+
}
104+
105+
/** @inheritdoc */
106+
public processEvent(event: Event, _hint: unknown, client: Client): Event {
99107
// Note: In the long run, most of the logic here should probably move into the request data utility functions. For
100108
// the moment it lives here, though, until https://github.com/getsentry/sentry-javascript/issues/5718 is addressed.
101109
// (TL;DR: Those functions touch many parts of the repo in many different ways, and need to be clened up. Once
102110
// that's happened, it will be easier to add this logic in without worrying about unexpected side effects.)
103111
const { transactionNamingScheme } = this._options;
104112

105-
addGlobalEventProcessor(event => {
106-
const hub = getCurrentHub();
107-
const self = hub.getIntegration(RequestData);
108-
109-
const { sdkProcessingMetadata = {} } = event;
110-
const req = sdkProcessingMetadata.request;
113+
const { sdkProcessingMetadata = {} } = event;
114+
const req = sdkProcessingMetadata.request;
111115

112-
// If the globally installed instance of this integration isn't associated with the current hub, `self` will be
113-
// undefined
114-
if (!self || !req) {
115-
return event;
116-
}
116+
if (!req) {
117+
return event;
118+
}
117119

118-
// The Express request handler takes a similar `include` option to that which can be passed to this integration.
119-
// If passed there, we store it in `sdkProcessingMetadata`. TODO(v8): Force express and GCP people to use this
120-
// integration, so that all of this passing and conversion isn't necessary
121-
const addRequestDataOptions =
122-
sdkProcessingMetadata.requestDataOptionsFromExpressHandler ||
123-
sdkProcessingMetadata.requestDataOptionsFromGCPWrapper ||
124-
convertReqDataIntegrationOptsToAddReqDataOpts(this._options);
120+
// The Express request handler takes a similar `include` option to that which can be passed to this integration.
121+
// If passed there, we store it in `sdkProcessingMetadata`. TODO(v8): Force express and GCP people to use this
122+
// integration, so that all of this passing and conversion isn't necessary
123+
const addRequestDataOptions =
124+
sdkProcessingMetadata.requestDataOptionsFromExpressHandler ||
125+
sdkProcessingMetadata.requestDataOptionsFromGCPWrapper ||
126+
convertReqDataIntegrationOptsToAddReqDataOpts(this._options);
125127

126-
const processedEvent = this._addRequestData(event, req, addRequestDataOptions);
128+
const processedEvent = this._addRequestData(event, req, addRequestDataOptions);
127129

128-
// Transaction events already have the right `transaction` value
129-
if (event.type === 'transaction' || transactionNamingScheme === 'handler') {
130-
return processedEvent;
131-
}
130+
// Transaction events already have the right `transaction` value
131+
if (event.type === 'transaction' || transactionNamingScheme === 'handler') {
132+
return processedEvent;
133+
}
132134

133-
// In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction`
134-
// value with a high-quality one
135-
const reqWithTransaction = req as { _sentryTransaction?: Transaction };
136-
const transaction = reqWithTransaction._sentryTransaction;
137-
if (transaction) {
138-
// TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to
139-
// keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential
140-
// to break things like alert rules.)
141-
const shouldIncludeMethodInTransactionName =
142-
getSDKName(hub) === 'sentry.javascript.nextjs'
143-
? transaction.name.startsWith('/api')
144-
: transactionNamingScheme !== 'path';
145-
146-
const [transactionValue] = extractPathForTransaction(req, {
147-
path: true,
148-
method: shouldIncludeMethodInTransactionName,
149-
customRoute: transaction.name,
150-
});
151-
152-
processedEvent.transaction = transactionValue;
153-
}
135+
// In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction`
136+
// value with a high-quality one
137+
const reqWithTransaction = req as { _sentryTransaction?: Transaction };
138+
const transaction = reqWithTransaction._sentryTransaction;
139+
if (transaction) {
140+
// TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to
141+
// keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential
142+
// to break things like alert rules.)
143+
const shouldIncludeMethodInTransactionName =
144+
getSDKName(client) === 'sentry.javascript.nextjs'
145+
? transaction.name.startsWith('/api')
146+
: transactionNamingScheme !== 'path';
147+
148+
const [transactionValue] = extractPathForTransaction(req, {
149+
path: true,
150+
method: shouldIncludeMethodInTransactionName,
151+
customRoute: transaction.name,
152+
});
153+
154+
processedEvent.transaction = transactionValue;
155+
}
154156

155-
return processedEvent;
156-
});
157+
return processedEvent;
157158
}
158159
}
159160

@@ -199,12 +200,12 @@ function convertReqDataIntegrationOptsToAddReqDataOpts(
199200
};
200201
}
201202

202-
function getSDKName(hub: Hub): string | undefined {
203+
function getSDKName(client: Client): string | undefined {
203204
try {
204205
// For a long chain like this, it's fewer bytes to combine a try-catch with assuming everything is there than to
205206
// write out a long chain of `a && a.b && a.b.c && ...`
206207
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
207-
return hub.getClient()!.getOptions()!._metadata!.sdk!.name;
208+
return client.getOptions()._metadata!.sdk!.name;
208209
} catch (err) {
209210
// In theory we should never get here
210211
return undefined;
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const COUNTER_METRIC_TYPE = 'c' as const;
2+
export const GAUGE_METRIC_TYPE = 'g' as const;
3+
export const SET_METRIC_TYPE = 's' as const;
4+
export const DISTRIBUTION_METRIC_TYPE = 'd' as const;
5+
6+
/**
7+
* Normalization regex for metric names and metric tag names.
8+
*
9+
* This enforces that names and tag keys only contain alphanumeric characters,
10+
* underscores, forward slashes, periods, and dashes.
11+
*
12+
* See: https://develop.sentry.dev/sdk/metrics/#normalization
13+
*/
14+
export const NAME_AND_TAG_KEY_NORMALIZATION_REGEX = /[^a-zA-Z0-9_/.-]+/g;
15+
16+
/**
17+
* Normalization regex for metric tag values.
18+
*
19+
* This enforces that values only contain words, digits, or the following
20+
* special characters: _:/@.{}[\]$-
21+
*
22+
* See: https://develop.sentry.dev/sdk/metrics/#normalization
23+
*/
24+
export const TAG_VALUE_NORMALIZATION_REGEX = /[^\w\d_:/@.{}[\]$-]+/g;
25+
26+
/**
27+
* This does not match spec in https://develop.sentry.dev/sdk/metrics
28+
* but was chosen to optimize for the most common case in browser environments.
29+
*/
30+
export const DEFAULT_FLUSH_INTERVAL = 5000;

0 commit comments

Comments
 (0)