@@ -5,6 +5,7 @@ import type { SentryExpoConfigOptions } from '../../src/js/tools/metroconfig';
55import {
66 getSentryExpoConfig ,
77 withSentryBabelTransformer ,
8+ withSentryExcludeServerOnlyResolver ,
89 withSentryFramesCollapsed ,
910 withSentryResolver ,
1011} from '../../src/js/tools/metroconfig' ;
@@ -362,6 +363,118 @@ describe('metroconfig', () => {
362363 }
363364 } ) ;
364365 } ) ;
366+ describe ( 'withSentryExcludeServerOnlyResolver' , ( ) => {
367+ const SENTRY_CORE_ORIGIN = '/project/node_modules/@sentry/core/build/esm/index.js' ;
368+
369+ let originalResolverMock : any ;
370+
371+ // @ts -expect-error Can't see type CustomResolutionContext
372+ let contextMock : CustomResolutionContext ;
373+ let config : MetroConfig = { } ;
374+
375+ beforeEach ( ( ) => {
376+ originalResolverMock = jest . fn ( ) ;
377+ contextMock = {
378+ resolveRequest : jest . fn ( ) ,
379+ originModulePath : SENTRY_CORE_ORIGIN ,
380+ } ;
381+
382+ config = {
383+ resolver : {
384+ resolveRequest : originalResolverMock ,
385+ } ,
386+ } ;
387+ } ) ;
388+
389+ describe . each ( [
390+ [ './integrations/mcp-server/index.js' ] ,
391+ [ './tracing/openai/index.js' ] ,
392+ [ './tracing/anthropic-ai/index.js' ] ,
393+ [ './tracing/google-genai/index.js' ] ,
394+ [ './tracing/vercel-ai/index.js' ] ,
395+ [ './tracing/langchain/index.js' ] ,
396+ [ './tracing/langgraph/index.js' ] ,
397+ [ './utils/ai/providerSkip.js' ] ,
398+ ] ) ( 'with server-only module %s from @sentry/core' , serverOnlyModule => {
399+ test ( 'removes module when platform is android' , ( ) => {
400+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( config ) ;
401+ const result = modifiedConfig . resolver ?. resolveRequest ?.( contextMock , serverOnlyModule , 'android' ) ;
402+
403+ expect ( result ) . toEqual ( { type : 'empty' } ) ;
404+ expect ( originalResolverMock ) . not . toHaveBeenCalled ( ) ;
405+ } ) ;
406+
407+ test ( 'removes module when platform is ios' , ( ) => {
408+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( config ) ;
409+ const result = modifiedConfig . resolver ?. resolveRequest ?.( contextMock , serverOnlyModule , 'ios' ) ;
410+
411+ expect ( result ) . toEqual ( { type : 'empty' } ) ;
412+ expect ( originalResolverMock ) . not . toHaveBeenCalled ( ) ;
413+ } ) ;
414+
415+ test ( 'keeps module when platform is web' , ( ) => {
416+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( config ) ;
417+ modifiedConfig . resolver ?. resolveRequest ?.( contextMock , serverOnlyModule , 'web' ) ;
418+
419+ expect ( originalResolverMock ) . toHaveBeenCalledWith ( contextMock , serverOnlyModule , 'web' ) ;
420+ } ) ;
421+
422+ test ( 'keeps module when platform is null' , ( ) => {
423+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( config ) ;
424+ modifiedConfig . resolver ?. resolveRequest ?.( contextMock , serverOnlyModule , null ) ;
425+
426+ expect ( originalResolverMock ) . toHaveBeenCalledWith ( contextMock , serverOnlyModule , null ) ;
427+ } ) ;
428+ } ) ;
429+
430+ test ( 'does not exclude modules when origin is not @sentry/core' , ( ) => {
431+ const nonSentryContext = {
432+ ...contextMock ,
433+ originModulePath : '/project/node_modules/some-other-package/index.js' ,
434+ } ;
435+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( config ) ;
436+ modifiedConfig . resolver ?. resolveRequest ?.( nonSentryContext , './tracing/openai/index.js' , 'android' ) ;
437+
438+ expect ( originalResolverMock ) . toHaveBeenCalledWith ( nonSentryContext , './tracing/openai/index.js' , 'android' ) ;
439+ } ) ;
440+
441+ test ( 'does not exclude modules when originModulePath is not available' , ( ) => {
442+ const noOriginContext = {
443+ resolveRequest : jest . fn ( ) ,
444+ } ;
445+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( config ) ;
446+ modifiedConfig . resolver ?. resolveRequest ?.( noOriginContext , './tracing/openai/index.js' , 'android' ) ;
447+
448+ expect ( originalResolverMock ) . toHaveBeenCalledWith ( noOriginContext , './tracing/openai/index.js' , 'android' ) ;
449+ } ) ;
450+
451+ test ( 'calls originalResolver for non-AI modules on native platforms' , ( ) => {
452+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( config ) ;
453+ modifiedConfig . resolver ?. resolveRequest ?.( contextMock , './exports.js' , 'android' ) ;
454+
455+ expect ( originalResolverMock ) . toHaveBeenCalledWith ( contextMock , './exports.js' , 'android' ) ;
456+ } ) ;
457+
458+ test ( 'falls back to context.resolveRequest when no originalResolver' , ( ) => {
459+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( { resolver : { } } ) ;
460+ modifiedConfig . resolver ?. resolveRequest ?.( contextMock , './exports.js' , 'android' ) ;
461+
462+ expect ( contextMock . resolveRequest ) . toHaveBeenCalledWith ( contextMock , './exports.js' , 'android' ) ;
463+ } ) ;
464+
465+ test ( 'exits process on old Metro when context.resolveRequest is the resolver itself (infinite recursion guard)' , ( ) => {
466+ // @ts -expect-error mock.
467+ const mockExit = jest . spyOn ( process , 'exit' ) . mockImplementation ( ( ) => { } ) ;
468+ const modifiedConfig = withSentryExcludeServerOnlyResolver ( { resolver : { } } ) ;
469+
470+ const resolver = modifiedConfig . resolver ?. resolveRequest ;
471+ // Simulate old Metro behavior where context.resolveRequest === the resolver itself
472+ const oldMetroContext = { resolveRequest : resolver } ;
473+ resolver ?.( oldMetroContext , './exports.js' , 'android' ) ;
474+
475+ expect ( mockExit ) . toHaveBeenCalledWith ( - 1 ) ;
476+ } ) ;
477+ } ) ;
365478} ) ;
366479
367480// function create mock metro frame
0 commit comments