diff --git a/.size-limit.js b/.size-limit.js index 5e94a923e656..51f1e0c711f7 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -115,7 +115,7 @@ module.exports = [ path: 'packages/browser/build/bundles/bundle.tracing.min.js', gzip: false, brotli: false, - limit: '105 KB', + limit: '112 KB', }, { name: '@sentry/browser - ES6 CDN Bundle (minified & uncompressed)', diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js new file mode 100644 index 000000000000..ef68afb06576 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/scenario.js @@ -0,0 +1,56 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + _experiments: { + metricsAggregator: true, + }, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + () => { + Sentry.metrics.increment('root-counter', 1, { + tags: { + email: 'jon.doe@example.com', + }, + }); + Sentry.metrics.increment('root-counter', 1, { + tags: { + email: 'jane.doe@example.com', + }, + }); + + Sentry.startSpan( + { + name: 'Some other span', + op: 'transaction', + }, + () => { + Sentry.metrics.increment('root-counter'); + Sentry.metrics.increment('root-counter'); + Sentry.metrics.increment('root-counter', 2); + + Sentry.metrics.set('root-set', 'some-value'); + Sentry.metrics.set('root-set', 'another-value'); + Sentry.metrics.set('root-set', 'another-value'); + + Sentry.metrics.gauge('root-gauge', 42); + Sentry.metrics.gauge('root-gauge', 20); + + Sentry.metrics.distribution('root-distribution', 42); + Sentry.metrics.distribution('root-distribution', 20); + }, + ); + }, +); diff --git a/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts new file mode 100644 index 000000000000..94f5fdc30c70 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/metric-summaries/test.ts @@ -0,0 +1,91 @@ +import { createRunner } from '../../../utils/runner'; + +const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + _metrics_summary: { + 'c:root-counter@none': [ + { + min: 1, + max: 1, + count: 1, + sum: 1, + tags: { + release: '1.0', + transaction: 'Test Transaction', + email: 'jon.doe@example.com', + }, + }, + { + min: 1, + max: 1, + count: 1, + sum: 1, + tags: { + release: '1.0', + transaction: 'Test Transaction', + email: 'jane.doe@example.com', + }, + }, + ], + }, + spans: expect.arrayContaining([ + expect.objectContaining({ + description: 'Some other span', + op: 'transaction', + _metrics_summary: { + 'c:root-counter@none': [ + { + min: 1, + max: 2, + count: 3, + sum: 4, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + ], + 's:root-set@none': [ + { + min: 0, + max: 1, + count: 3, + sum: 2, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + ], + 'g:root-gauge@none': [ + { + min: 20, + max: 42, + count: 2, + sum: 62, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + ], + 'd:root-distribution@none': [ + { + min: 20, + max: 42, + count: 2, + sum: 62, + tags: { + release: '1.0', + transaction: 'Test Transaction', + }, + }, + ], + }, + }), + ]), +}; + +test('Should add metric summaries to spans', done => { + createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); +}); diff --git a/packages/angular-ivy/scripts/prepack.ts b/packages/angular-ivy/scripts/prepack.ts index b9ec3f0d787f..bc9159954b39 100644 --- a/packages/angular-ivy/scripts/prepack.ts +++ b/packages/angular-ivy/scripts/prepack.ts @@ -6,6 +6,7 @@ type PackageJson = { type?: string; nx?: string; volta?: any; + exports?: Record>; }; const buildDir = path.join(process.cwd(), 'build'); @@ -18,6 +19,18 @@ const pkgJson: PackageJson = JSON.parse(fs.readFileSync(pkjJsonPath).toString()) delete pkgJson.main; pkgJson.type = 'module'; +pkgJson.exports = { + '.': { + es2015: './fesm2015/sentry-angular-ivy.js', + esm2015: './esm2015/sentry-angular-ivy.js', + fesm2015: './fesm2015/sentry-angular-ivy.js', + import: './fesm2015/sentry-angular-ivy.js', + require: './bundles/sentry-angular-ivy.umd.js', + types: './sentry-angular-ivy.d.ts', + }, + './*': './*', +}; + // no need to keep around other properties that are only relevant for our reop: delete pkgJson.nx; delete pkgJson.volta; diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 5b2f74615c45..43c6227b0f8e 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -129,7 +129,6 @@ export class TraceService implements OnDestroy { if (!getActiveSpan()) { startBrowserTracingNavigationSpan(client, { name: strippedUrl, - op: 'navigation', origin: 'auto.navigation.angular', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 98e5486894db..e4e94e23b894 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -93,6 +93,10 @@ export { continueTrace, cron, parameterize, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index e9e947de8559..1b959e05c397 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -76,6 +76,13 @@ export { parameterize, } from '@sentry/core'; +export { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, +} from '@sentry/core'; + export { WINDOW } from './helpers'; export { BrowserClient } from './client'; export { makeFetchTransport, makeXHRTransport } from './transports'; diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 2be5c71c4518..59ef74cdbfb5 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -27,6 +27,7 @@ export { // eslint-disable-next-line deprecation/deprecation Replay, replayIntegration, + getReplay, } from '@sentry/replay'; export type { ReplayEventType, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index b51083052c8b..363e25d93371 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -117,6 +117,10 @@ export { onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, spotlightIntegration, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/node'; export { BunClient } from './client'; diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts index 6a49fda5918b..2b331082ab3e 100644 --- a/packages/core/src/metrics/aggregator.ts +++ b/packages/core/src/metrics/aggregator.ts @@ -6,8 +6,9 @@ import type { Primitive, } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; +import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; import { METRIC_MAP } from './instance'; +import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -62,7 +63,11 @@ export class MetricsAggregator implements MetricsAggregatorBase { const tags = sanitizeTags(unsanitizedTags); const bucketKey = getBucketKey(metricType, name, unit, tags); + let bucketItem = this._buckets.get(bucketKey); + // If this is a set metric, we need to calculate the delta from the previous weight. + const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; + if (bucketItem) { bucketItem.metric.add(value); // TODO(abhi): Do we need this check? @@ -82,6 +87,10 @@ export class MetricsAggregator implements MetricsAggregatorBase { this._buckets.set(bucketKey, bucketItem); } + // If value is a string, it's a set metric so calculate the delta from the previous weight. + const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; + updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); + // We need to keep track of the total weight of the buckets so that we can // flush them when we exceed the max weight. this._bucketsTotalWeight += bucketItem.metric.weight; diff --git a/packages/core/src/metrics/browser-aggregator.ts b/packages/core/src/metrics/browser-aggregator.ts index 5b5c81353024..40cfa1d404ab 100644 --- a/packages/core/src/metrics/browser-aggregator.ts +++ b/packages/core/src/metrics/browser-aggregator.ts @@ -1,14 +1,8 @@ -import type { - Client, - ClientOptions, - MeasurementUnit, - MetricBucketItem, - MetricsAggregator, - Primitive, -} from '@sentry/types'; +import type { Client, ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; +import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; import { METRIC_MAP } from './instance'; +import { updateMetricSummaryOnActiveSpan } from './metric-summary'; import type { MetricBucket, MetricType } from './types'; import { getBucketKey, sanitizeTags } from './utils'; @@ -46,7 +40,11 @@ export class BrowserMetricsAggregator implements MetricsAggregator { const tags = sanitizeTags(unsanitizedTags); const bucketKey = getBucketKey(metricType, name, unit, tags); - const bucketItem: MetricBucketItem | undefined = this._buckets.get(bucketKey); + + let bucketItem = this._buckets.get(bucketKey); + // If this is a set metric, we need to calculate the delta from the previous weight. + const previousWeight = bucketItem && metricType === SET_METRIC_TYPE ? bucketItem.metric.weight : 0; + if (bucketItem) { bucketItem.metric.add(value); // TODO(abhi): Do we need this check? @@ -54,7 +52,7 @@ export class BrowserMetricsAggregator implements MetricsAggregator { bucketItem.timestamp = timestamp; } } else { - this._buckets.set(bucketKey, { + bucketItem = { // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. metric: new METRIC_MAP[metricType](value), timestamp, @@ -62,8 +60,13 @@ export class BrowserMetricsAggregator implements MetricsAggregator { name, unit, tags, - }); + }; + this._buckets.set(bucketKey, bucketItem); } + + // If value is a string, it's a set metric so calculate the delta from the previous weight. + const val = typeof value === 'string' ? bucketItem.metric.weight - previousWeight : value; + updateMetricSummaryOnActiveSpan(metricType, name, val, unit, unsanitizedTags, bucketKey); } /** diff --git a/packages/core/src/metrics/constants.ts b/packages/core/src/metrics/constants.ts index e89e0fd1562b..a5f3a87f57d5 100644 --- a/packages/core/src/metrics/constants.ts +++ b/packages/core/src/metrics/constants.ts @@ -21,7 +21,7 @@ export const NAME_AND_TAG_KEY_NORMALIZATION_REGEX = /[^a-zA-Z0-9_/.-]+/g; * * See: https://develop.sentry.dev/sdk/metrics/#normalization */ -export const TAG_VALUE_NORMALIZATION_REGEX = /[^\w\d_:/@.{}[\]$-]+/g; +export const TAG_VALUE_NORMALIZATION_REGEX = /[^\w\d\s_:/@.{}[\]$-]+/g; /** * This does not match spec in https://develop.sentry.dev/sdk/metrics diff --git a/packages/core/src/metrics/metric-summary.ts b/packages/core/src/metrics/metric-summary.ts new file mode 100644 index 000000000000..bf2e828dae1b --- /dev/null +++ b/packages/core/src/metrics/metric-summary.ts @@ -0,0 +1,91 @@ +import type { MeasurementUnit, Span } from '@sentry/types'; +import type { MetricSummary } from '@sentry/types'; +import type { Primitive } from '@sentry/types'; +import { dropUndefinedKeys } from '@sentry/utils'; +import { getActiveSpan } from '../tracing'; +import type { MetricType } from './types'; + +/** + * key: bucketKey + * value: [exportKey, MetricSummary] + */ +type MetricSummaryStorage = Map; + +let SPAN_METRIC_SUMMARY: WeakMap | undefined; + +function getMetricStorageForSpan(span: Span): MetricSummaryStorage | undefined { + return SPAN_METRIC_SUMMARY ? SPAN_METRIC_SUMMARY.get(span) : undefined; +} + +/** + * Fetches the metric summary if it exists for the passed span + */ +export function getMetricSummaryJsonForSpan(span: Span): Record> | undefined { + const storage = getMetricStorageForSpan(span); + + if (!storage) { + return undefined; + } + const output: Record> = {}; + + for (const [, [exportKey, summary]] of storage) { + if (!output[exportKey]) { + output[exportKey] = []; + } + + output[exportKey].push(dropUndefinedKeys(summary)); + } + + return output; +} + +/** + * Updates the metric summary on the currently active span + */ +export function updateMetricSummaryOnActiveSpan( + metricType: MetricType, + sanitizedName: string, + value: number, + unit: MeasurementUnit, + tags: Record, + bucketKey: string, +): void { + const span = getActiveSpan(); + if (span) { + const storage = getMetricStorageForSpan(span) || new Map(); + + const exportKey = `${metricType}:${sanitizedName}@${unit}`; + const bucketItem = storage.get(bucketKey); + + if (bucketItem) { + const [, summary] = bucketItem; + storage.set(bucketKey, [ + exportKey, + { + min: Math.min(summary.min, value), + max: Math.max(summary.max, value), + count: (summary.count += 1), + sum: (summary.sum += value), + tags: summary.tags, + }, + ]); + } else { + storage.set(bucketKey, [ + exportKey, + { + min: value, + max: value, + count: 1, + sum: value, + tags, + }, + ]); + } + + if (!SPAN_METRIC_SUMMARY) { + SPAN_METRIC_SUMMARY = new WeakMap(); + } + + SPAN_METRIC_SUMMARY.set(span, storage); + } +} diff --git a/packages/core/src/metrics/utils.ts b/packages/core/src/metrics/utils.ts index a6674bcf30e1..7b1cf96a8462 100644 --- a/packages/core/src/metrics/utils.ts +++ b/packages/core/src/metrics/utils.ts @@ -62,7 +62,7 @@ export function sanitizeTags(unsanitizedTags: Record): Record for (const key in unsanitizedTags) { if (Object.prototype.hasOwnProperty.call(unsanitizedTags, key)) { const sanitizedKey = key.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); - tags[sanitizedKey] = String(unsanitizedTags[key]).replace(TAG_VALUE_NORMALIZATION_REGEX, '_'); + tags[sanitizedKey] = String(unsanitizedTags[key]).replace(TAG_VALUE_NORMALIZATION_REGEX, ''); } } return tags; diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 28a2de56475d..1c178fc05fbe 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -16,6 +16,7 @@ import type { import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import { getRootSpan } from '../utils/getRootSpan'; import { @@ -624,6 +625,7 @@ export class Span implements SpanInterface { timestamp: this._endTime, trace_id: this._traceId, origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + _metrics_summary: getMetricSummaryJsonForSpan(this), }); } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 026723929471..709aa628f42e 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -15,6 +15,7 @@ import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; +import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; @@ -331,6 +332,7 @@ export class Transaction extends SpanClass implements TransactionInterface { capturedSpanIsolationScope, dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, + _metrics_summary: getMetricSummaryJsonForSpan(this), ...(source && { transaction_info: { source, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index d42ad97fedb8..9a25923b2c42 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -85,7 +85,12 @@ export { linkedErrorsIntegration, functionToStringIntegration, requestDataIntegration, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/core'; + export type { SpanStatusType } from '@sentry/core'; export { DenoClient } from './client'; diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index f4c47998ea90..9046ead7c7fc 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -11,7 +11,7 @@ import type { ExtendedBackburner } from '@sentry/ember/runloop'; import type { Span } from '@sentry/types'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { BrowserClient } from '..'; import { getActiveSpan, startInactiveSpan } from '..'; import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig } from '../types'; @@ -115,17 +115,18 @@ export function _instrumentEmberRouter( browserTracingOptions.instrumentPageLoad !== false ) { const routeInfo = routerService.recognize(url); - Sentry.startBrowserTracingPageLoadSpan(client, { + activeRootSpan = Sentry.startBrowserTracingPageLoadSpan(client, { name: `route:${routeInfo.name}`, - op: 'pageload', origin: 'auto.pageload.ember', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, tags: { url, toRoute: routeInfo.name, 'routing.instrumentation': '@sentry/ember', }, }); - activeRootSpan = getActiveSpan(); } const finishActiveTransaction = (_: unknown, nextInstance: unknown): void => { @@ -147,10 +148,12 @@ export function _instrumentEmberRouter( const { fromRoute, toRoute } = getTransitionInformation(transition, routerService); activeRootSpan?.end(); - Sentry.startBrowserTracingNavigationSpan(client, { + activeRootSpan = Sentry.startBrowserTracingNavigationSpan(client, { name: `route:${toRoute}`, - op: 'navigation', origin: 'auto.navigation.ember', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, tags: { fromRoute, toRoute, @@ -158,8 +161,6 @@ export function _instrumentEmberRouter( }, }); - activeRootSpan = getActiveSpan(); - transitionSpan = startInactiveSpan({ attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index b5ffeb6de0c9..ce3f4f85a214 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -98,6 +98,10 @@ export { functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/node'; export type { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 8d0a82ecbfe6..0efd5b3923a2 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -90,7 +90,16 @@ export { linkedErrorsIntegration, requestDataIntegration, } from '@sentry/core'; + +export { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, +} from '@sentry/core'; + export type { SpanStatusType } from '@sentry/core'; + export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; export { NodeClient } from './client'; diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index a34250100287..b50f1334aa0a 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -104,6 +104,10 @@ export { runWithAsyncContext, // eslint-disable-next-line deprecation/deprecation enableAnrDetection, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/node'; // Keeping the `*` exports for backwards compatibility and types diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts index 6471ff9e87ce..d16fd7733c20 100644 --- a/packages/replay/src/index.ts +++ b/packages/replay/src/index.ts @@ -19,5 +19,7 @@ export type { CanvasManagerOptions, } from './types'; +export { getReplay } from './util/getReplay'; + // TODO (v8): Remove deprecated types export * from './types/deprecated'; diff --git a/packages/replay/src/util/getReplay.ts b/packages/replay/src/util/getReplay.ts new file mode 100644 index 000000000000..278505f15338 --- /dev/null +++ b/packages/replay/src/util/getReplay.ts @@ -0,0 +1,13 @@ +import { getClient } from '@sentry/core'; +import type { replayIntegration } from '../integration'; + +/** + * This is a small utility to get a type-safe instance of the Replay integration. + */ +// eslint-disable-next-line deprecation/deprecation +export function getReplay(): ReturnType | undefined { + const client = getClient(); + return ( + client && client.getIntegrationByName && client.getIntegrationByName>('Replay') + ); +} diff --git a/packages/replay/test/unit/util/getReplay.test.ts b/packages/replay/test/unit/util/getReplay.test.ts new file mode 100644 index 000000000000..7f614d4fdc33 --- /dev/null +++ b/packages/replay/test/unit/util/getReplay.test.ts @@ -0,0 +1,42 @@ +import { getCurrentScope } from '@sentry/core'; +import { replayIntegration } from '../../../src/integration'; +import { getReplay } from '../../../src/util/getReplay'; +import { getDefaultClientOptions, init } from '../../utils/TestClient'; + +describe('getReplay', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + + it('works without a client', () => { + const actual = getReplay(); + expect(actual).toBeUndefined(); + }); + + it('works with a client without Replay', () => { + init( + getDefaultClientOptions({ + dsn: 'https://dsn@ingest.f00.f00/1', + }), + ); + + const actual = getReplay(); + expect(actual).toBeUndefined(); + }); + + it('works with a client with Replay xxx', () => { + const replay = replayIntegration(); + init( + getDefaultClientOptions({ + integrations: [replay], + replaysOnErrorSampleRate: 0, + replaysSessionSampleRate: 0, + }), + ); + + const actual = getReplay(); + expect(actual).toBeDefined(); + expect(actual === replay).toBe(true); + expect(replay.getReplayId()).toBe(undefined); + }); +}); diff --git a/packages/replay/test/utils/TestClient.ts b/packages/replay/test/utils/TestClient.ts index da131aec8fd2..26a14f2a9795 100644 --- a/packages/replay/test/utils/TestClient.ts +++ b/packages/replay/test/utils/TestClient.ts @@ -39,7 +39,7 @@ export function init(options: TestClientOptions): void { initAndBind(TestClient, options); } -export function getDefaultClientOptions(options: Partial = {}): ClientOptions { +export function getDefaultClientOptions(options: Partial = {}): ClientOptions { return { integrations: [], dsn: 'https://username@domain/123', diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 24ee21115f0b..34917a4b28e4 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -107,4 +107,8 @@ export { httpIntegration, nativeNodeFetchintegration, spotlightIntegration, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/node'; diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index 14528959d34e..3e454bdb364a 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -4,7 +4,7 @@ import { addNonEnumerableProperty, objectify } from '@sentry/utils'; import type { LoadEvent } from '@sveltejs/kit'; import type { SentryWrappedFlag } from '../common/utils'; -import { isRedirect } from '../common/utils'; +import { isHttpError, isRedirect } from '../common/utils'; type PatchedLoadEvent = LoadEvent & Partial; @@ -14,7 +14,11 @@ function sendErrorToSentry(e: unknown): unknown { const objectifiedErr = objectify(e); // We don't want to capture thrown `Redirect`s as these are not errors but expected behaviour - if (isRedirect(objectifiedErr)) { + // Neither 4xx errors, given that they are not valuable. + if ( + isRedirect(objectifiedErr) || + (isHttpError(objectifiedErr) && objectifiedErr.status < 500 && objectifiedErr.status >= 400) + ) { return objectifiedErr; } diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 7a886334cfbc..e3e03e4ca127 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -99,6 +99,10 @@ export { runWithAsyncContext, // eslint-disable-next-line deprecation/deprecation enableAnrDetection, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index eea862e9d901..2f5fd28ac1f8 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -166,7 +166,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi // eslint-disable-next-line no-console debug && console.log('[Source Maps Plugin] Flattening source maps'); - jsFiles.forEach(async file => { + for (const file of jsFiles) { try { await (sorcery as Sorcery).load(file).then(async chain => { if (!chain) { @@ -202,7 +202,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi ); await fs.promises.writeFile(mapFile, cleanedMapContent); } - }); + } try { // @ts-expect-error - this hook exists on the plugin! diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts index e839b5a9cba5..ca7c1d625224 100644 --- a/packages/sveltekit/test/client/load.test.ts +++ b/packages/sveltekit/test/client/load.test.ts @@ -69,6 +69,30 @@ describe('wrapLoadWithSentry', () => { expect(mockCaptureException).not.toHaveBeenCalled(); }); + it.each([400, 404, 499])("doesn't call captureException for thrown `HttpError`s with status %s", async status => { + async function load(_: Parameters[0]): Promise> { + throw { status, body: 'error' }; + } + + const wrappedLoad = wrapLoadWithSentry(load); + const res = wrappedLoad(MOCK_LOAD_ARGS); + await expect(res).rejects.toThrow(); + + expect(mockCaptureException).not.toHaveBeenCalled(); + }); + + it.each([500, 501, 599])('calls captureException for thrown `HttpError`s with status %s', async status => { + async function load(_: Parameters[0]): Promise> { + throw { status, body: 'error' }; + } + + const wrappedLoad = wrapLoadWithSentry(load); + const res = wrappedLoad(MOCK_LOAD_ARGS); + await expect(res).rejects.toThrow(); + + expect(mockCaptureException).toHaveBeenCalledTimes(1); + }); + describe('calls trace function', async () => { it('creates a load span', async () => { async function load({ params }: Parameters[0]): Promise> { diff --git a/packages/tracing-internal/src/browser/browserTracingIntegration.ts b/packages/tracing-internal/src/browser/browserTracingIntegration.ts index 31660eff00a7..b27575e147cf 100644 --- a/packages/tracing-internal/src/browser/browserTracingIntegration.ts +++ b/packages/tracing-internal/src/browser/browserTracingIntegration.ts @@ -1,5 +1,6 @@ -/* eslint-disable max-lines, complexity */ +/* eslint-disable max-lines */ import type { IdleTransaction } from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -237,7 +238,6 @@ export const browserTracingIntegration = ((_options: Partial { @@ -325,7 +328,10 @@ export const browserTracingIntegration = ((_options: Partial { + // Clean up JSDom + Object.defineProperty(WINDOW, 'document', { value: originalGlobalDocument }); + Object.defineProperty(WINDOW, 'location', { value: originalGlobalLocation }); + Object.defineProperty(WINDOW, 'history', { value: originalGlobalHistory }); +}); + +describe('browserTracingIntegration', () => { + afterEach(() => { + getCurrentScope().clear(); + }); + + it('works with tracing enabled', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(true); + expect(spanToJSON(span!)).toEqual({ + description: '/', + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + + it('works with tracing disabled', () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + it('works with tracing enabled but unsampled', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 0, + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + conditionalTest({ min: 10 })('navigation', () => { + it('starts navigation when URL changes', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan(); + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(true); + expect(span!.isRecording()).toBe(true); + expect(spanToJSON(span!)).toEqual({ + description: '/', + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + // this is what is used to get the span name - JSDOM does not update this on it's own! + const dom = new JSDOM(undefined, { url: 'https://example.com/test' }); + Object.defineProperty(global, 'location', { value: dom.window.document.location, writable: true }); + + WINDOW.history.pushState({}, '', '/test'); + + expect(span!.isRecording()).toBe(false); + + const span2 = getActiveSpan(); + expect(span2).toBeDefined(); + expect(spanIsSampled(span2!)).toBe(true); + expect(span2!.isRecording()).toBe(true); + expect(spanToJSON(span2!)).toEqual({ + description: '/test', + op: 'navigation', + origin: 'auto.navigation.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + + // this is what is used to get the span name - JSDOM does not update this on it's own! + const dom2 = new JSDOM(undefined, { url: 'https://example.com/test2' }); + Object.defineProperty(global, 'location', { value: dom2.window.document.location, writable: true }); + + WINDOW.history.pushState({}, '', '/test2'); + + expect(span2!.isRecording()).toBe(false); + + const span3 = getActiveSpan(); + expect(span3).toBeDefined(); + expect(spanIsSampled(span3!)).toBe(true); + expect(span3!.isRecording()).toBe(true); + expect(spanToJSON(span3!)).toEqual({ + description: '/test2', + op: 'navigation', + origin: 'auto.navigation.browser', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + }); + + describe('startBrowserTracingPageLoadSpan', () => { + it('works without integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { name: 'test span' }); + + expect(span).toBeUndefined(); + }); + + it('works with unsampled span', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 0, + integrations: [browserTracingIntegration({ instrumentPageLoad: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + it('works with integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentPageLoad: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'pageload', + origin: 'manual', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + expect(spanIsSampled(span!)).toBe(true); + }); + + it('allows to overwrite properties', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentPageLoad: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingPageLoadSpan(client, { + name: 'test span', + origin: 'auto.test', + attributes: { testy: 'yes' }, + }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'pageload', + origin: 'auto.test', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + testy: 'yes', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + }); + + describe('startBrowserTracingNavigationSpan', () => { + it('works without integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + integrations: [], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { name: 'test span' }); + + expect(span).toBeUndefined(); + }); + + it('works with unsampled span', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 0, + integrations: [browserTracingIntegration({ instrumentNavigation: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanIsSampled(span!)).toBe(false); + }); + + it('works with integration setup', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentNavigation: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { name: 'test span' }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'navigation', + origin: 'manual', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + expect(spanIsSampled(span!)).toBe(true); + }); + + it('allows to overwrite properties', () => { + const client = new TestClient( + getDefaultClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentNavigation: false })], + }), + ); + setCurrentClient(client); + client.init(); + + const span = startBrowserTracingNavigationSpan(client, { + name: 'test span', + origin: 'auto.test', + attributes: { testy: 'yes' }, + }); + + expect(span).toBeDefined(); + expect(spanToJSON(span!)).toEqual({ + description: 'test span', + op: 'navigation', + origin: 'auto.test', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + testy: 'yes', + }, + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }); + }); + }); +}); diff --git a/packages/tracing-internal/test/utils/utils.ts b/packages/tracing-internal/test/utils/utils.ts new file mode 100644 index 000000000000..0652be303ed4 --- /dev/null +++ b/packages/tracing-internal/test/utils/utils.ts @@ -0,0 +1,20 @@ +import { parseSemver } from '@sentry/utils'; + +export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; + +/** + * Returns`describe` or `describe.skip` depending on allowed major versions of Node. + * + * @param {{ min?: number; max?: number }} allowedVersion + * @return {*} {jest.Describe} + */ +export const conditionalTest = (allowedVersion: { min?: number; max?: number }): jest.Describe => { + const major = NODE_VERSION.major; + if (!major) { + return describe.skip as jest.Describe; + } + + return major < (allowedVersion.min || -Infinity) || major > (allowedVersion.max || Infinity) + ? (describe.skip as jest.Describe) + : (describe as any); +}; diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 50322f18fbc6..15b253c666c8 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -11,7 +11,7 @@ import type { Request } from './request'; import type { CaptureContext } from './scope'; import type { SdkInfo } from './sdkinfo'; import type { Severity, SeverityLevel } from './severity'; -import type { Span, SpanJSON } from './span'; +import type { MetricSummary, Span, SpanJSON } from './span'; import type { Thread } from './thread'; import type { TransactionSource } from './transaction'; import type { User } from './user'; @@ -73,6 +73,7 @@ export interface ErrorEvent extends Event { } export interface TransactionEvent extends Event { type: 'transaction'; + _metrics_summary?: Record>; } /** JSDoc */ diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5970383febc3..d4fcd439ae4a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -99,6 +99,7 @@ export type { SpanJSON, SpanContextData, TraceFlag, + MetricSummary, } from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; @@ -150,5 +151,9 @@ export type { export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions'; export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin'; -export type { MetricsAggregator, MetricBucketItem, MetricInstance } from './metrics'; +export type { + MetricsAggregator, + MetricBucketItem, + MetricInstance, +} from './metrics'; export type { ParameterizedString } from './parameterize'; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index fc7f1077368e..6aa6ea1113f6 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -31,6 +31,14 @@ export type SpanAttributes = Partial<{ }> & Record; +export type MetricSummary = { + min: number; + max: number; + count: number; + sum: number; + tags?: Record | undefined; +}; + /** This type is aligned with the OpenTelemetry TimeInput type. */ export type SpanTimeInput = HrTime | number | Date; @@ -47,6 +55,7 @@ export interface SpanJSON { timestamp?: number; trace_id: string; origin?: SpanOrigin; + _metrics_summary?: Record>; } // These are aligned with OpenTelemetry trace flags diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 8288c8ca5374..fb3624de22f2 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -86,6 +86,10 @@ export { inboundFiltersIntegration, linkedErrorsIntegration, requestDataIntegration, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core';