Skip to content

Consolidate duplicated low-level helpers #22003

Description

@logaretm

We re-implement the same small helpers (type guards, coercions, safe serializers) across many packages, often looser or subtly different from what already lives in other packages. This is a maintainability and correctness cleanup, not a bundle-size one as I think it wouldn't amount to much.

This came up after noticing we keep getting those isRecord utils, probably from agents liking it too much. Still it creates a bit of a situation where we have competing or duplicate utils that only differ in name or slight behavior mismatch.

I did a scan over what could be consolidated to our now three main util sources: server-utils, browser-utils and core depending on where the utils are duplicated and which runtimes they are relevant to.

Stuff to consolidate in core

  • isRecord (server-utils/src/vercel-ai/util.ts:19), isObject (browser/src/integrations/graphqlClient.ts:218), isObject (node-core/src/integrations/winston.ts:133). All identical typeof === 'object' && x !== null, and all intentionally accept arrays unlike core's existing isPlainObject.
  • arrayify / toArray (bundler-plugins/src/core/utils.ts:17, plus aws vendored copies). Identical Array.isArray(x) ? x : [x]. Leave the vendored aws copies.
  • uniq (inline): tanstackstart-react (routePatterns.ts:71, autoInstrumentMiddleware.ts:182), aws-serverless (MessageAttributes.ts:76), server-utils (orchestrion/config/index.ts:29) use [...new Set()]; vue (tracing.ts:65) uses filter+indexOf (O(n^2)). Same output, vue variant is the odd one.
  • Nullish guards isEmptyValue: nitro (captureStorageEvents.ts:102) + nuxt (instrumentStorage.ts:215). Byte-identical v === null || v === undefined.
  • safeStringify (server-utils/src/vercel-ai/util.ts:24) vs core-internal getJsonString (tracing/ai/utils.ts:198, NOT exported from the barrel). Identical except safeStringify adds a try/catch and returns '[unserializable]', while getJsonString throws on circular refs.
  • addOriginToSpan: cloudflare (utils/addOriginToSpan.ts:6), node-core (utils/addOriginToSpan.ts:6), opentelemetry (utils/addOriginToSpan.ts:6). Byte-identical including imports. cloudflare only depends on core (not opentelemetry), so core is the shared home. Sets SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, already core-exported.
  • lowQualityTransactionsFilter: nuxt (server/sdk.ts:58) + solidstart (server/utils.ts:25) + react-router (integration variant) (+ hono patterns). Shared scaffolding (transaction-type guard, Object.assign(fn, { id }), log-then-return-null), but the match predicates are genuinely per-framework. Share a makeTransactionFilter(id, predicate); keep predicates local.
  • parseEventHintOrCaptureContext: node-core (utils/prepareEvent.ts:17) duplicates core's exported copy.
  • isStaticAssetRequest: node-core (integrations/http/httpServerSpansIntegration.ts:292) duplicates core's exported copy.

Consolidate into @sentry/browser-utils

  • isElement: already re-homed here (browser-utils/src/is.ts:6) and adds a try/catch core lacks. Core's copy is deprecated.
  • htmlTreeAsString: browser-utils (htmlTreeAsString.ts:55) vs core (utils/browser.ts:20, exported). Divergent, should drop core's.

Consolidate into @sentry/server-utils

  • isAsyncIterable: server-utils tracing-channel/anthropic.ts:138 + openai.ts:116. Byte-identical intra-package duplicate. Hoist into one server-utils util (no API change).
  • getVercelEnv: nextjs (common/getVercelEnv.ts:6) + vercel-edge (utils/vercel.ts:10). Byte-identical bodies (vercel-edge only adds a local declare const process).
  • flushSafelyWithTimeout: nextjs (common/utils/responseEnd.ts:48) + nuxt (server/sdk.ts:123). Byte-identical (comment wording differs only). Core already has an internal flushWithTimeout (utils/flushIfServerless.ts:11) with the same shape - export it (or a flushSafely) and have both import it.
  • customRewriteFramesIntegration: nextjs (server + edge - and these two already drifted) + sveltekit. Only the defineIntegration wrapper is shared; the iteratees are framework-specific. De-dup the nextjs server/edge pair first; sharing the thin wrapper is optional.

Maybe in node-core?

  • markEventUnhandled: aws-serverless (utils.ts:16) + google-cloud-serverless (utils.ts:46). Byte-identical. Both depend on node-core (not server-utils), so node-core is the home.

Metadata

Metadata

Assignees

No one assigned

    Labels

    TaskjavascriptPull requests that update javascript code

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions