From 42b71f5095e72306f1dee89af223aa57ccb0b5a8 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 5 Oct 2026 15:42:41 +0200 Subject: [PATCH] ref: Reuse isObjectLike for object guards Co-Authored-By: OpenAI Codex --- packages/cloudflare/src/options.ts | 2 +- packages/deno/src/opentelemetry/tracer.ts | 4 ++-- packages/nestjs/src/helpers.ts | 17 ++++++++--------- packages/nestjs/src/setup.ts | 4 ++-- .../nestjs/test/sentry-global-filter.test.ts | 3 ++- .../routing/appRouterRoutingInstrumentation.ts | 5 +++-- .../wrapAppGetInitialPropsWithSentry.ts | 11 ++++++----- .../wrapErrorGetInitialPropsWithSentry.ts | 7 ++++--- .../wrapGetInitialPropsWithSentry.ts | 7 ++++--- .../wrapGetServerSidePropsWithSentry.ts | 3 ++- packages/react-router/src/client/utils.ts | 9 +++++---- packages/remix/src/v3/server/errorFilter.ts | 6 +++--- packages/remix/test/v3/errorFilter.test.ts | 6 +++++- .../hono/defaultShouldHandleError.ts | 6 ++++-- .../src/integrations/kafkajs/spans.ts | 3 ++- .../server-utils/src/integrations/langgraph.ts | 4 ++-- .../server-utils/src/integrations/mastra.ts | 2 +- 17 files changed, 56 insertions(+), 43 deletions(-) diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index a90a236e4bf1..0d65340b306b 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -39,7 +39,7 @@ function getEnvVar>(env: unknown, varName: key * @returns The final options. */ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions { - if (typeof env !== 'object' || env === null) { + if (!isObjectLike(env)) { return userOptions; } diff --git a/packages/deno/src/opentelemetry/tracer.ts b/packages/deno/src/opentelemetry/tracer.ts index e9846b994e45..ce3516feece4 100644 --- a/packages/deno/src/opentelemetry/tracer.ts +++ b/packages/deno/src/opentelemetry/tracer.ts @@ -1,7 +1,7 @@ import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import { startInactiveSpan, startSpanManual } from '@sentry/core'; +import { isObjectLike, startInactiveSpan, startSpanManual } from '@sentry/core'; /** * Set up a mock OTEL tracer to allow inter-op with OpenTelemetry emitted spans. @@ -58,7 +58,7 @@ class SentryDenoTracer implements Tracer { context?: unknown, fn?: F, ): ReturnType { - const opts = (typeof options === 'object' && options !== null ? options : {}) as SpanOptions; + const opts = (isObjectLike(options) ? options : {}) as SpanOptions; const spanOpts = { ...opts, diff --git a/packages/nestjs/src/helpers.ts b/packages/nestjs/src/helpers.ts index 58d03dda019f..56f9a5704b32 100644 --- a/packages/nestjs/src/helpers.ts +++ b/packages/nestjs/src/helpers.ts @@ -1,3 +1,5 @@ +import { isObjectLike } from '@sentry/core'; + /** * Determines if the exception is an expected NestJS control flow error. * - HttpException have getStatus and getResponse methods: https://github.com/nestjs/nest/blob/master/packages/microservices/exceptions/rpc-exception.ts @@ -10,17 +12,15 @@ * @returns `true` if the exception is expected and should not be reported to Sentry, otherwise `false`. */ export function isExpectedError(exception: unknown): boolean { - if (typeof exception !== 'object' || exception === null) { + if (!isObjectLike(exception)) { return false; } - const ex = exception as Record; - // HttpException if ( - typeof ex.getStatus === 'function' && - typeof ex.getResponse === 'function' && - typeof ex.initMessage === 'function' + typeof exception.getStatus === 'function' && + typeof exception.getResponse === 'function' && + typeof exception.initMessage === 'function' ) { return true; } @@ -39,10 +39,9 @@ export function isExpectedError(exception: unknown): boolean { * We use duck-typing to avoid importing from `@nestjs/websockets` or `@nestjs/microservices`. */ export function isWsOrRpcException(exception: unknown): boolean { - if (typeof exception !== 'object' || exception === null) { + if (!isObjectLike(exception)) { return false; } - const ex = exception as Record; - return typeof ex.getError === 'function' && typeof ex.initMessage === 'function'; + return typeof exception.getError === 'function' && typeof exception.initMessage === 'function'; } diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index b646806532b4..a8e56a9d8a66 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -8,7 +8,7 @@ import type { } from '@nestjs/common'; import { Catch, Global, HttpException, Injectable, Logger, Module } from '@nestjs/common'; import { APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core'; -import { captureException, debug, getDefaultIsolationScope, getIsolationScope } from '@sentry/core'; +import { captureException, debug, getDefaultIsolationScope, getIsolationScope, isObjectLike } from '@sentry/core'; import type { Observable } from 'rxjs'; import { isExpectedError, isWsOrRpcException } from './helpers'; @@ -166,7 +166,7 @@ class SentryGlobalFilter extends BaseExceptionFilter { if (isWsOrRpcException(exception)) { const result = (exception as { getError: () => unknown }).getError(); - const response = typeof result === 'object' && result !== null ? result : { status: 'error', message: result }; + const response = isObjectLike(result) ? result : { status: 'error', message: result }; client.emit?.('exception', response); return; } diff --git a/packages/nestjs/test/sentry-global-filter.test.ts b/packages/nestjs/test/sentry-global-filter.test.ts index d5f772992854..90c340aa2992 100644 --- a/packages/nestjs/test/sentry-global-filter.test.ts +++ b/packages/nestjs/test/sentry-global-filter.test.ts @@ -11,7 +11,8 @@ vi.mock('../src/helpers', () => ({ isWsOrRpcException: vi.fn(), })); -vi.mock('@sentry/core', () => ({ +vi.mock('@sentry/core', async importOriginal => ({ + isObjectLike: (await importOriginal()).isObjectLike, captureException: vi.fn().mockReturnValue('mock-event-id'), getIsolationScope: vi.fn(), getDefaultIsolationScope: vi.fn(), diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 26133c1d9747..4b7e9b1c7ae5 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -2,6 +2,7 @@ import type { Client, Span } from '@sentry/core'; import { GLOBAL_OBJ, hasSpanStreamingEnabled, + isObjectLike, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, filterCollectedUrl, @@ -250,8 +251,8 @@ export function appRouterInstrumentNavigation(client: Client): void { if (globalValue) { GLOBAL_OBJ_WITH_NEXT_ROUTER.next = new Proxy(globalValue, { set(target, p, newValue) { - if (p === 'router' && typeof newValue === 'object' && newValue !== null) { - patchRouter(client, newValue, currentRouterPatchingNavigationSpanRef); + if (p === 'router' && isObjectLike(newValue)) { + patchRouter(client, newValue as unknown as NextRouter, currentRouterPatchingNavigationSpanRef); } // @ts-expect-error we cannot possibly type this diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts index 02c54e76b3d6..7fda6a1da56e 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type App from 'next/app'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -45,23 +46,23 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI baggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); - if (typeof appGetInitialProps === 'object' && appGetInitialProps !== null) { + if (isObjectLike(appGetInitialProps)) { // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per // https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`. // For this reason, we just handle the case where `pageProps` doesn't exist explicitly. - if (!(appGetInitialProps as Record).pageProps) { - (appGetInitialProps as Record).pageProps = {}; + if (!appGetInitialProps.pageProps) { + appGetInitialProps.pageProps = {}; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { - (appGetInitialProps as { pageProps: Record }).pageProps._sentryTraceData = sentryTrace; + (appGetInitialProps.pageProps as Record)._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (appGetInitialProps as { pageProps: Record }).pageProps._sentryBaggage = baggage; + (appGetInitialProps.pageProps as Record)._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts index 209ca486db15..1346b78ffd25 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; import { isBuild } from '../utils/isBuild'; @@ -47,15 +48,15 @@ export function wrapErrorGetInitialPropsWithSentry( sentryTrace?: string; } = await tracedGetInitialProps.apply(thisArg, args); - if (typeof errorGetInitialProps === 'object' && errorGetInitialProps !== null) { + if (isObjectLike(errorGetInitialProps)) { if (sentryTrace) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - (errorGetInitialProps as Record)._sentryTraceData = sentryTrace; + errorGetInitialProps._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (errorGetInitialProps as Record)._sentryBaggage = baggage; + errorGetInitialProps._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts index f597692820b8..82fc76343785 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { NextPage } from 'next'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -43,15 +44,15 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro sentryTrace?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. - if (typeof initialProps === 'object' && initialProps !== null) { + if (isObjectLike(initialProps)) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { - (initialProps as Record)._sentryTraceData = sentryTrace; + initialProps._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (initialProps as Record)._sentryBaggage = baggage; + initialProps._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts index 5512c6204410..f99faa528f91 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { GetServerSideProps } from 'next'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -39,7 +40,7 @@ export function wrapGetServerSidePropsWithSentry( sentryTrace?: string; } = await (tracedGetServerSideProps.apply(thisArg, args) as ReturnType); - if (typeof serverSideProps === 'object' && serverSideProps !== null && 'props' in serverSideProps) { + if (isObjectLike(serverSideProps) && 'props' in serverSideProps) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { (serverSideProps.props as Record)._sentryTraceData = sentryTrace; diff --git a/packages/react-router/src/client/utils.ts b/packages/react-router/src/client/utils.ts index 94044c096c94..eeb029f44878 100644 --- a/packages/react-router/src/client/utils.ts +++ b/packages/react-router/src/client/utils.ts @@ -4,6 +4,7 @@ import { getClient, GLOBAL_OBJ, hasSpanStreamingEnabled, + isObjectLike, NAVIGATION_SPAN_NAME_FALLBACK, filterCollectedUrl, } from '@sentry/core'; @@ -24,13 +25,13 @@ const GLOBAL_OBJ_WITH_DATA_ROUTER = GLOBAL_OBJ as typeof GLOBAL_OBJ & { * to avoid "[object Object]" transaction names. */ export function resolveNavigateArg(target: unknown): string { - if (typeof target !== 'object' || target === null) { + if (!isObjectLike(target)) { // string or number return String(target); } // Object `to` with pathname - const pathname = (target as Record).pathname; + const pathname = target.pathname; if (typeof pathname === 'string') { return pathname || '/'; } @@ -46,12 +47,12 @@ export function resolveNavigateArg(target: unknown): string { * destination the user is navigating to, including any query string. */ export function resolveNavigateUrl(target: unknown): string { - if (typeof target !== 'object' || target === null) { + if (!isObjectLike(target)) { // string or number return String(target); } - const { pathname, search, hash } = target as Record; + const { pathname, search, hash } = target; const path = typeof pathname === 'string' && pathname !== '' ? pathname : WINDOW.location?.pathname || '/'; return `${path}${typeof search === 'string' ? search : ''}${typeof hash === 'string' ? hash : ''}`; diff --git a/packages/remix/src/v3/server/errorFilter.ts b/packages/remix/src/v3/server/errorFilter.ts index 9e98a2874a27..5a771746f8b0 100644 --- a/packages/remix/src/v3/server/errorFilter.ts +++ b/packages/remix/src/v3/server/errorFilter.ts @@ -1,4 +1,4 @@ -import { captureException } from '@sentry/core'; +import { captureException, isObjectLike } from '@sentry/core'; /** Decides whether a thrown value should become a Sentry issue. */ export type ShouldHandleError = (error: unknown) => boolean; @@ -8,11 +8,11 @@ export type ShouldHandleError = (error: unknown) => boolean; * outcome, not a fault, and the request still produces a span. */ export function defaultShouldHandleError(error: unknown): boolean { - if (typeof error !== 'object' || error === null) { + if (!isObjectLike(error)) { return true; } - const status = (error as { status?: unknown }).status; + const status = error.status; return !(typeof status === 'number' && status >= 300 && status < 500); } diff --git a/packages/remix/test/v3/errorFilter.test.ts b/packages/remix/test/v3/errorFilter.test.ts index f307f8bdd4e4..ab17dbedadea 100644 --- a/packages/remix/test/v3/errorFilter.test.ts +++ b/packages/remix/test/v3/errorFilter.test.ts @@ -1,7 +1,11 @@ +import type * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const captureException = vi.fn(); -vi.mock('@sentry/core', () => ({ captureException: (...args: unknown[]) => captureException(...args) })); +vi.mock('@sentry/core', async importOriginal => ({ + isObjectLike: (await importOriginal()).isObjectLike, + captureException: (...args: unknown[]) => captureException(...args), +})); const { captureRequestError, defaultShouldHandleError, isRequestAbort, setShouldHandleError } = await import('../../src/v3/server/errorFilter'); diff --git a/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts b/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts index 71eab3bbb3fd..c166faea6384 100644 --- a/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts +++ b/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts @@ -1,3 +1,5 @@ +import { isObjectLike } from '@sentry/core'; + /** * Default implementation of the `shouldHandleError` callback. * @@ -9,11 +11,11 @@ * Hono's `HTTPException`, third-party middleware errors, and custom error subclasses. */ export function defaultShouldHandleError(error: unknown): boolean { - if (typeof error !== 'object' || error === null) { + if (!isObjectLike(error)) { return true; } - const status = (error as { status?: unknown }).status; + const status = error.status; return !(typeof status === 'number' && status >= 300 && status < 500); } diff --git a/packages/server-utils/src/integrations/kafkajs/spans.ts b/packages/server-utils/src/integrations/kafkajs/spans.ts index fdae5a0d46bd..3ed0ca5a84bd 100644 --- a/packages/server-utils/src/integrations/kafkajs/spans.ts +++ b/packages/server-utils/src/integrations/kafkajs/spans.ts @@ -26,6 +26,7 @@ import { getClient, getTraceData, hasSpanStreamingEnabled, + isObjectLike, propagationContextFromHeaders, SPAN_STATUS_ERROR, startInactiveSpan, @@ -183,7 +184,7 @@ export function applyErrorToSpans(spans: Span[], reason: unknown): void { let errorType: string = ERROR_TYPE_VALUE_OTHER; if (typeof reason === 'string' || reason === undefined) { errorMessage = reason; - } else if (typeof reason === 'object' && reason !== null && Object.prototype.hasOwnProperty.call(reason, 'message')) { + } else if (isObjectLike(reason) && Object.prototype.hasOwnProperty.call(reason, 'message')) { errorMessage = (reason as { message?: string }).message; errorType = (reason as { constructor: { name: string } }).constructor.name; } diff --git a/packages/server-utils/src/integrations/langgraph.ts b/packages/server-utils/src/integrations/langgraph.ts index a6b9f6f21541..5a41a8296aa3 100644 --- a/packages/server-utils/src/integrations/langgraph.ts +++ b/packages/server-utils/src/integrations/langgraph.ts @@ -1,6 +1,6 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; -import { debug, defineIntegration } from '@sentry/core'; +import { debug, defineIntegration, isObjectLike } from '@sentry/core'; import { resolveAIRecordingOptions } from '../ai/core/utils'; import { createLangChainCallbackHandler } from '../ai/langchain'; import { instrumentCompiledGraphInvoke } from '../ai/langgraph'; @@ -95,7 +95,7 @@ function instrumentLanggraph(options: LangGraphOptions): void { function getFirstArgObject(args: unknown[] | undefined): Record | undefined { const first = (args ?? [])[0]; - return typeof first === 'object' && first !== null ? (first as Record) : undefined; + return isObjectLike(first) ? first : undefined; } /** diff --git a/packages/server-utils/src/integrations/mastra.ts b/packages/server-utils/src/integrations/mastra.ts index cfd1c5c70995..9856ef51651d 100644 --- a/packages/server-utils/src/integrations/mastra.ts +++ b/packages/server-utils/src/integrations/mastra.ts @@ -176,7 +176,7 @@ function instrumentExecuteWithContext(): void { } function attachExporter(instance: unknown, options: MastraOptions): void { - if (typeof instance !== 'object' || instance === null || registered.has(instance)) { + if (!isObjectLike(instance) || registered.has(instance)) { return; }