diff --git a/packages/aws-serverless/src/integration/aws/vendored/services/MessageAttributes.ts b/packages/aws-serverless/src/integration/aws/vendored/services/MessageAttributes.ts index bea959401a65..b891e5880afc 100644 --- a/packages/aws-serverless/src/integration/aws/vendored/services/MessageAttributes.ts +++ b/packages/aws-serverless/src/integration/aws/vendored/services/MessageAttributes.ts @@ -8,6 +8,7 @@ */ import { TextMapGetter, TextMapSetter, context, propagation, diag } from '@opentelemetry/api'; +import { uniq } from '@sentry/core'; import type { SQS, SNS } from '../aws-sdk.types'; // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-quotas.html @@ -72,7 +73,5 @@ export const addPropagationFieldsToAttributeNames = ( messageAttributeNames: string[] = [], propagationFields: string[], ) => { - return messageAttributeNames.length - ? Array.from(new Set([...messageAttributeNames, ...propagationFields])) - : propagationFields; + return messageAttributeNames.length ? uniq([...messageAttributeNames, ...propagationFields]) : propagationFields; }; diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index ffd08180b202..339520c5d194 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -121,6 +121,7 @@ export { getTraceData } from './utils/traceData'; export { shouldPropagateTraceForUrl } from './utils/tracePropagationTargets'; export { getTraceMetaTags } from './utils/meta'; export { debounce } from './utils/debounce'; +export { uniq } from './utils/array'; export { makeWeakRef, derefWeakRef } from './utils/weakRef'; export type { MaybeWeakRef } from './utils/weakRef'; export { shouldIgnoreSpan } from './utils/should-ignore-span'; diff --git a/packages/core/src/utils/array.ts b/packages/core/src/utils/array.ts new file mode 100644 index 000000000000..6ae7e7ed0bbc --- /dev/null +++ b/packages/core/src/utils/array.ts @@ -0,0 +1,9 @@ +/** + * Return a new array with duplicate values removed, preserving first-occurrence order. + * + * @param input the array to deduplicate + * @returns a new array containing each distinct value once, in the order it first appeared + */ +export function uniq(input: T[]): T[] { + return Array.from(new Set(input)); +} diff --git a/packages/core/test/lib/utils/array.test.ts b/packages/core/test/lib/utils/array.test.ts new file mode 100644 index 000000000000..53529739291a --- /dev/null +++ b/packages/core/test/lib/utils/array.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { uniq } from '../../../src/utils/array'; + +describe('Unit | util | uniq', () => { + it('removes duplicate values', () => { + expect(uniq([1, 1, 2, 3, 3, 3])).toEqual([1, 2, 3]); + }); + + it('preserves first-occurrence order', () => { + expect(uniq(['b', 'a', 'b', 'c', 'a'])).toEqual(['b', 'a', 'c']); + }); + + it('returns an empty array for an empty input', () => { + expect(uniq([])).toEqual([]); + }); + + it('returns a new array and does not mutate the input', () => { + const input = [1, 2, 2]; + const result = uniq(input); + expect(result).not.toBe(input); + expect(input).toEqual([1, 2, 2]); + }); + + it('dedupes by identity, keeping distinct object references', () => { + const a = { id: 1 }; + const b = { id: 1 }; + expect(uniq([a, a, b])).toEqual([a, b]); + }); +}); diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index fe35d317621e..5d26bf19ee87 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -1,4 +1,5 @@ import type { InstrumentationConfig } from '@apm-js-collab/code-transformer'; +import { uniq } from '@sentry/core'; import { mysqlConfig } from './mysql'; import { lruMemoizerConfig } from './lru-memoizer'; import { ioredisConfig } from './ioredis'; @@ -40,7 +41,7 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ * runtime and never passes through the code transform's `onLoad`, so its * diagnostics_channel calls are silently never injected. */ -export const INSTRUMENTED_MODULE_NAMES: string[] = Array.from(new Set(SENTRY_INSTRUMENTATIONS.map(i => i.module.name))); +export const INSTRUMENTED_MODULE_NAMES: string[] = uniq(SENTRY_INSTRUMENTATIONS.map(i => i.module.name)); /** * Returns `external` with any instrumented packages removed, so a bundler that diff --git a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts index 07cf2a29e832..266653f90a36 100644 --- a/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts +++ b/packages/tanstackstart-react/src/vite/autoInstrumentMiddleware.ts @@ -1,3 +1,4 @@ +import { uniq } from '@sentry/core'; import type { Plugin } from 'vite'; type AutoInstrumentMiddlewareOptions = { @@ -179,7 +180,7 @@ export function arrayToObjectShorthand(contents: string): string | null { } // Deduplicate to avoid invalid syntax like { foo, foo } - const uniqueItems = [...new Set(items)]; + const uniqueItems = uniq(items); return `{ ${uniqueItems.join(', ')} }`; } diff --git a/packages/tanstackstart-react/src/vite/routePatterns.ts b/packages/tanstackstart-react/src/vite/routePatterns.ts index db6a2b031635..9c90b863931c 100644 --- a/packages/tanstackstart-react/src/vite/routePatterns.ts +++ b/packages/tanstackstart-react/src/vite/routePatterns.ts @@ -1,5 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; +import { uniq } from '@sentry/core'; import type { Plugin } from 'vite'; /** @@ -68,7 +69,7 @@ export function extractRoutePatterns(content: string): string[] { } } - return [...new Set(patterns)].sort((a, b) => { + return uniq(patterns).sort((a, b) => { const aSegments = a.split('/'); const bSegments = b.split('/'); if (bSegments.length !== aSegments.length) { diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index f4708ddbd865..f33c998701eb 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,6 +1,6 @@ import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; -import { debug, timestampInSeconds } from '@sentry/core'; +import { debug, timestampInSeconds, uniq } from '@sentry/core'; import { DEFAULT_HOOKS } from './constants'; import { DEBUG_BUILD } from './debug-build'; import type { Hook, Operation, TracingOptions, ViewModel, Vue } from './types'; @@ -59,10 +59,7 @@ export function findTrackComponent(trackComponents: string[], formattedName: str } export const createTracingMixins = (options: Partial = {}): Mixins => { - const hooks = (options.hooks || []) - .concat(DEFAULT_HOOKS) - // Removing potential duplicates - .filter((value, index, self) => self.indexOf(value) === index); + const hooks = uniq((options.hooks || []).concat(DEFAULT_HOOKS)); const mixins: Mixins = {};