diff --git a/packages/opentelemetry/src/custom/getCurrentHub.ts b/packages/core/src/getCurrentHubShim.ts similarity index 84% rename from packages/opentelemetry/src/custom/getCurrentHub.ts rename to packages/core/src/getCurrentHubShim.ts index 9db09297d670..435004ce9f57 100644 --- a/packages/opentelemetry/src/custom/getCurrentHub.ts +++ b/packages/core/src/getCurrentHubShim.ts @@ -1,12 +1,9 @@ import type { Client, EventHint, Hub, Integration, IntegrationClass, SeverityLevel } from '@sentry/types'; - +import { addBreadcrumb } from './breadcrumbs'; +import { getClient, getCurrentScope, getIsolationScope, withScope } from './currentScopes'; import { - addBreadcrumb, captureEvent, endSession, - getClient, - getCurrentScope, - getIsolationScope, setContext, setExtra, setExtras, @@ -14,14 +11,14 @@ import { setTags, setUser, startSession, - withScope, -} from '@sentry/core'; +} from './exports'; /** * This is for legacy reasons, and returns a proxy object instead of a hub to be used. + * * @deprecated Use the methods directly. */ -export function getCurrentHub(): Hub { +export function getCurrentHubShim(): Hub { return { bindClient(client: Client): void { const scope = getCurrentScope(); @@ -48,7 +45,8 @@ export function getCurrentHub(): Hub { setContext, getIntegration(integration: IntegrationClass): T | null { - return getClient()?.getIntegrationByName(integration.id) || null; + const client = getClient(); + return (client && client.getIntegrationByName(integration.id)) || null; }, startSession, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 48bb5baf6afc..e3f827605aeb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -106,3 +106,6 @@ export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch'; export { trpcMiddleware } from './trpc'; + +// eslint-disable-next-line deprecation/deprecation +export { getCurrentHubShim } from './getCurrentHubShim'; diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts index 8b56d190b88a..8752d2a10df7 100644 --- a/packages/core/src/metrics/aggregator.ts +++ b/packages/core/src/metrics/aggregator.ts @@ -1,11 +1,11 @@ import type { Client, MeasurementUnit, MetricsAggregator as MetricsAggregatorBase, Primitive } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils'; -import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; +import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, SET_METRIC_TYPE } from './constants'; import { captureAggregateMetrics } from './envelope'; import { METRIC_MAP } from './instance'; import type { MetricBucket, MetricType } from './types'; -import { getBucketKey, sanitizeTags } from './utils'; +import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils'; /** * A metrics aggregator that aggregates metrics in memory and flushes them periodically. @@ -46,6 +46,7 @@ export class MetricsAggregator implements MetricsAggregatorBase { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access this._interval.unref(); } + this._flushShift = Math.floor((Math.random() * DEFAULT_FLUSH_INTERVAL) / 1000); this._forceFlush = false; } @@ -57,13 +58,14 @@ export class MetricsAggregator implements MetricsAggregatorBase { metricType: MetricType, unsanitizedName: string, value: number | string, - unit: MeasurementUnit = 'none', + unsanitizedUnit: MeasurementUnit = 'none', unsanitizedTags: Record = {}, maybeFloatTimestamp = timestampInSeconds(), ): void { const timestamp = Math.floor(maybeFloatTimestamp); - const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); + const name = sanitizeMetricKey(unsanitizedName); const tags = sanitizeTags(unsanitizedTags); + const unit = sanitizeUnit(unsanitizedUnit as string); const bucketKey = getBucketKey(metricType, name, unit, tags); diff --git a/packages/core/src/metrics/browser-aggregator.ts b/packages/core/src/metrics/browser-aggregator.ts index 7d599f5aeba8..e087b45ca010 100644 --- a/packages/core/src/metrics/browser-aggregator.ts +++ b/packages/core/src/metrics/browser-aggregator.ts @@ -1,11 +1,11 @@ import type { Client, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; import { updateMetricSummaryOnActiveSpan } from '../utils/spanUtils'; -import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX, SET_METRIC_TYPE } from './constants'; +import { DEFAULT_BROWSER_FLUSH_INTERVAL, SET_METRIC_TYPE } from './constants'; import { captureAggregateMetrics } from './envelope'; import { METRIC_MAP } from './instance'; import type { MetricBucket, MetricType } from './types'; -import { getBucketKey, sanitizeTags } from './utils'; +import { getBucketKey, sanitizeMetricKey, sanitizeTags, sanitizeUnit } from './utils'; /** * A simple metrics aggregator that aggregates metrics in memory and flushes them periodically. @@ -32,13 +32,14 @@ export class BrowserMetricsAggregator implements MetricsAggregator { metricType: MetricType, unsanitizedName: string, value: number | string, - unit: MeasurementUnit | undefined = 'none', + unsanitizedUnit: MeasurementUnit | undefined = 'none', unsanitizedTags: Record | undefined = {}, maybeFloatTimestamp: number | undefined = timestampInSeconds(), ): void { const timestamp = Math.floor(maybeFloatTimestamp); - const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); + const name = sanitizeMetricKey(unsanitizedName); const tags = sanitizeTags(unsanitizedTags); + const unit = sanitizeUnit(unsanitizedUnit as string); const bucketKey = getBucketKey(metricType, name, unit, tags); @@ -79,8 +80,7 @@ export class BrowserMetricsAggregator implements MetricsAggregator { return; } - // TODO(@anonrig): Use Object.values() when we support ES6+ - const metricBuckets = Array.from(this._buckets).map(([, bucketItem]) => bucketItem); + const metricBuckets = Array.from(this._buckets.values()); captureAggregateMetrics(this._client, metricBuckets); this._buckets.clear(); diff --git a/packages/core/src/metrics/constants.ts b/packages/core/src/metrics/constants.ts index a5f3a87f57d5..ae1cd968723c 100644 --- a/packages/core/src/metrics/constants.ts +++ b/packages/core/src/metrics/constants.ts @@ -3,26 +3,6 @@ export const GAUGE_METRIC_TYPE = 'g' as const; export const SET_METRIC_TYPE = 's' as const; export const DISTRIBUTION_METRIC_TYPE = 'd' as const; -/** - * Normalization regex for metric names and metric tag names. - * - * This enforces that names and tag keys only contain alphanumeric characters, - * underscores, forward slashes, periods, and dashes. - * - * See: https://develop.sentry.dev/sdk/metrics/#normalization - */ -export const NAME_AND_TAG_KEY_NORMALIZATION_REGEX = /[^a-zA-Z0-9_/.-]+/g; - -/** - * Normalization regex for metric tag values. - * - * This enforces that values only contain words, digits, or the following - * special characters: _:/@.{}[\]$- - * - * See: https://develop.sentry.dev/sdk/metrics/#normalization - */ -export const TAG_VALUE_NORMALIZATION_REGEX = /[^\w\d\s_:/@.{}[\]$-]+/g; - /** * This does not match spec in https://develop.sentry.dev/sdk/metrics * but was chosen to optimize for the most common case in browser environments. diff --git a/packages/core/src/metrics/utils.ts b/packages/core/src/metrics/utils.ts index 7b1cf96a8462..bc1ff93e0002 100644 --- a/packages/core/src/metrics/utils.ts +++ b/packages/core/src/metrics/utils.ts @@ -1,6 +1,5 @@ import type { MeasurementUnit, MetricBucketItem, Primitive } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; -import { NAME_AND_TAG_KEY_NORMALIZATION_REGEX, TAG_VALUE_NORMALIZATION_REGEX } from './constants'; import type { MetricType } from './types'; /** @@ -54,6 +53,63 @@ export function serializeMetricBuckets(metricBucketItems: MetricBucketItem[]): s return out; } +/** + * Sanitizes units + * + * These Regex's are straight from the normalisation docs: + * https://develop.sentry.dev/sdk/metrics/#normalization + */ +export function sanitizeUnit(unit: string): string { + return unit.replace(/[^\w]+/gi, '_'); +} + +/** + * Sanitizes metric keys + * + * These Regex's are straight from the normalisation docs: + * https://develop.sentry.dev/sdk/metrics/#normalization + */ +export function sanitizeMetricKey(key: string): string { + return key.replace(/[^\w\-.]+/gi, '_'); +} + +/** + * Sanitizes metric keys + * + * These Regex's are straight from the normalisation docs: + * https://develop.sentry.dev/sdk/metrics/#normalization + */ +function sanitizeTagKey(key: string): string { + return key.replace(/[^\w\-./]+/gi, ''); +} + +/** + * These Regex's are straight from the normalisation docs: + * https://develop.sentry.dev/sdk/metrics/#normalization + */ +const tagValueReplacements: [string, string][] = [ + ['\n', '\\n'], + ['\r', '\\r'], + ['\t', '\\t'], + ['\\', '\\\\'], + ['|', '\\u{7c}'], + [',', '\\u{2c}'], +]; + +function getCharOrReplacement(input: string): string { + for (const [search, replacement] of tagValueReplacements) { + if (input === search) { + return replacement; + } + } + + return input; +} + +function sanitizeTagValue(value: string): string { + return [...value].reduce((acc, char) => acc + getCharOrReplacement(char), ''); +} + /** * Sanitizes tags. */ @@ -61,8 +117,8 @@ export function sanitizeTags(unsanitizedTags: Record): Record const tags: 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, ''); + const sanitizedKey = sanitizeTagKey(key); + tags[sanitizedKey] = sanitizeTagValue(String(unsanitizedTags[key])); } } return tags; diff --git a/packages/core/test/lib/metrics/utils.test.ts b/packages/core/test/lib/metrics/utils.test.ts index fe96404b72ea..e25014715748 100644 --- a/packages/core/test/lib/metrics/utils.test.ts +++ b/packages/core/test/lib/metrics/utils.test.ts @@ -4,7 +4,7 @@ import { GAUGE_METRIC_TYPE, SET_METRIC_TYPE, } from '../../../src/metrics/constants'; -import { getBucketKey } from '../../../src/metrics/utils'; +import { getBucketKey, sanitizeTags } from '../../../src/metrics/utils'; describe('getBucketKey', () => { it.each([ @@ -18,4 +18,26 @@ describe('getBucketKey', () => { ])('should return', (metricType, name, unit, tags, expected) => { expect(getBucketKey(metricType, name, unit, tags)).toEqual(expected); }); + + it('should sanitize tags', () => { + const inputTags = { + 'f-oo|bar': '%$foo/', + 'foo$.$.$bar': 'blah{}', + 'foö-bar': 'snöwmän', + route: 'GET /foo', + __bar__: 'this | or , that', + 'foo/': 'hello!\n\r\t\\', + }; + + const outputTags = { + 'f-oobar': '%$foo/', + 'foo..bar': 'blah{}', + 'fo-bar': 'snöwmän', + route: 'GET /foo', + __bar__: 'this \\u{7c} or \\u{2c} that', + 'foo/': 'hello!\\n\\r\\t\\\\', + }; + + expect(sanitizeTags(inputTags)).toEqual(outputTags); + }); }); diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index b7f66870d07f..e28995a5d805 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -1,5 +1,10 @@ import * as api from '@opentelemetry/api'; -import { getDefaultCurrentScope, getDefaultIsolationScope, setAsyncContextStrategy } from '@sentry/core'; +import { + getCurrentHubShim, + getDefaultCurrentScope, + getDefaultIsolationScope, + setAsyncContextStrategy, +} from '@sentry/core'; import type { withActiveSpan as defaultWithActiveSpan } from '@sentry/core'; import type { Hub, Scope } from '@sentry/types'; @@ -8,7 +13,6 @@ import { SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, } from './constants'; -import { getCurrentHub as _getCurrentHub } from './custom/getCurrentHub'; import { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace'; import type { CurrentScopes } from './types'; import { getScopesFromContext } from './utils/contextData'; @@ -38,7 +42,7 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { function getCurrentHub(): Hub { // eslint-disable-next-line deprecation/deprecation - const hub = _getCurrentHub(); + const hub = getCurrentHubShim(); return { ...hub, getScope: () => { diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index 3d727c707897..db5abb951f4c 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -28,7 +28,7 @@ export { suppressTracing } from './utils/suppressTracing'; // eslint-disable-next-line deprecation/deprecation export { setupGlobalHub } from './custom/hub'; // eslint-disable-next-line deprecation/deprecation -export { getCurrentHub } from './custom/getCurrentHub'; +export { getCurrentHubShim } from '@sentry/core'; export { setupEventContextTrace } from './setupEventContextTrace'; export { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrategy'; diff --git a/packages/vercel-edge/src/async.ts b/packages/vercel-edge/src/async.ts index 50e2d80ad652..d0a9b8644d3f 100644 --- a/packages/vercel-edge/src/async.ts +++ b/packages/vercel-edge/src/async.ts @@ -1,5 +1,9 @@ -import { Hub as HubClass, getGlobalHub } from '@sentry/core'; -import { setAsyncContextStrategy } from '@sentry/core'; +import { + getCurrentHubShim, + getDefaultCurrentScope, + getDefaultIsolationScope, + setAsyncContextStrategy, +} from '@sentry/core'; import type { Hub, Scope } from '@sentry/types'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; @@ -11,7 +15,7 @@ interface AsyncLocalStorage { run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; } -let asyncStorage: AsyncLocalStorage; +let asyncStorage: AsyncLocalStorage<{ scope: Scope; isolationScope: Scope }>; /** * Sets the async context strategy to use AsyncLocalStorage which should be available in the edge runtime. @@ -32,68 +36,63 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void { asyncStorage = new MaybeGlobalAsyncLocalStorage(); } - function getCurrentAsyncStorageHub(): Hub | undefined { - return asyncStorage.getStore(); + function getScopes(): { scope: Scope; isolationScope: Scope } { + const scopes = asyncStorage.getStore(); + + if (scopes) { + return scopes; + } + + // fallback behavior: + // if, for whatever reason, we can't find scopes on the context here, we have to fix this somehow + return { + scope: getDefaultCurrentScope(), + isolationScope: getDefaultIsolationScope(), + }; } function getCurrentHub(): Hub { - return getCurrentAsyncStorageHub() || getGlobalHub(); + // eslint-disable-next-line deprecation/deprecation + const hub = getCurrentHubShim(); + return { + ...hub, + getScope: () => { + const scopes = getScopes(); + return scopes.scope; + }, + getIsolationScope: () => { + const scopes = getScopes(); + return scopes.isolationScope; + }, + }; } function withScope(callback: (scope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const isolationScope = parentHub.getIsolationScope(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { + const scope = getScopes().scope.clone(); + const isolationScope = getScopes().isolationScope; + return asyncStorage.run({ scope, isolationScope }, () => { return callback(scope); }); } function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const isolationScope = parentHub.getIsolationScope(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { + const isolationScope = getScopes().isolationScope.clone(); + return asyncStorage.run({ scope, isolationScope }, () => { return callback(scope); }); } function withIsolationScope(callback: (isolationScope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const isolationScope = parentHub.getIsolationScope().clone(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { + const scope = getScopes().scope; + const isolationScope = getScopes().isolationScope.clone(); + return asyncStorage.run({ scope, isolationScope }, () => { return callback(isolationScope); }); } function withSetIsolationScope(isolationScope: Scope, callback: (isolationScope: Scope) => T): T { - const parentHub = getCurrentHub(); - - /* eslint-disable deprecation/deprecation */ - const client = parentHub.getClient(); - const scope = parentHub.getScope().clone(); - const newHub = new HubClass(client, scope, isolationScope); - /* eslint-enable deprecation/deprecation */ - - return asyncStorage.run(newHub, () => { + const scope = getScopes().scope; + return asyncStorage.run({ scope, isolationScope }, () => { return callback(isolationScope); }); } @@ -104,9 +103,7 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void { withSetScope, withIsolationScope, withSetIsolationScope, - // eslint-disable-next-line deprecation/deprecation - getCurrentScope: () => getCurrentHub().getScope(), - // eslint-disable-next-line deprecation/deprecation - getIsolationScope: () => getCurrentHub().getIsolationScope(), + getCurrentScope: () => getScopes().scope, + getIsolationScope: () => getScopes().isolationScope, }); }