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.
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
isRecordutils, 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-utilsandcoredepending on where the utils are duplicated and which runtimes they are relevant to.Stuff to consolidate in
coreisRecord(server-utils/src/vercel-ai/util.ts:19),isObject(browser/src/integrations/graphqlClient.ts:218),isObject(node-core/src/integrations/winston.ts:133). All identicaltypeof === 'object' && x !== null, and all intentionally accept arrays unlike core's existingisPlainObject.arrayify/toArray(bundler-plugins/src/core/utils.ts:17, plus aws vendored copies). IdenticalArray.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) usesfilter+indexOf(O(n^2)). Same output, vue variant is the odd one.isEmptyValue: nitro (captureStorageEvents.ts:102) + nuxt (instrumentStorage.ts:215). Byte-identicalv === null || v === undefined.safeStringify(server-utils/src/vercel-ai/util.ts:24) vs core-internalgetJsonString(tracing/ai/utils.ts:198, NOT exported from the barrel). Identical exceptsafeStringifyadds a try/catch and returns'[unserializable]', whilegetJsonStringthrows 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. SetsSEMANTIC_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 amakeTransactionFilter(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 localdeclare const process).flushSafelyWithTimeout: nextjs (common/utils/responseEnd.ts:48) + nuxt (server/sdk.ts:123). Byte-identical (comment wording differs only). Core already has an internalflushWithTimeout(utils/flushIfServerless.ts:11) with the same shape - export it (or aflushSafely) and have both import it.customRewriteFramesIntegration: nextjs (server + edge - and these two already drifted) + sveltekit. Only thedefineIntegrationwrapper 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.