Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/core/src/js/tools/metroconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function withSentryConfig(
if (includeWebReplay === false) {
newConfig = withSentryResolver(newConfig, includeWebReplay);
}
newConfig = withSentryExcludeServerOnlyResolver(newConfig);
if (enableSourceContextInDevelopment) {
newConfig = withSentryMiddleware(newConfig);
}
Expand Down Expand Up @@ -128,6 +129,7 @@ export function getSentryExpoConfig(
if (options.includeWebReplay === false) {
newConfig = withSentryResolver(newConfig, options.includeWebReplay);
}
newConfig = withSentryExcludeServerOnlyResolver(newConfig);

if (options.enableSourceContextInDevelopment ?? true) {
newConfig = withSentryMiddleware(newConfig);
Expand Down Expand Up @@ -274,6 +276,49 @@ Please follow one of the following options:
};
}

const SENTRY_CORE_SERVER_ONLY_MODULE_RE =
/@sentry\/core\/.*\/(mcp-server|tracing\/(vercel-ai|openai|anthropic-ai|google-genai|langchain|langgraph)|utils\/ai)\//;
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated

/**
* Excludes server-only AI/MCP modules from native (Android/iOS) bundles.
*/
export function withSentryExcludeServerOnlyResolver(config: MetroConfig): MetroConfig {
const originalResolver = config.resolver?.resolveRequest as CustomResolver | CustomResolverBeforeMetro068 | undefined;

const sentryServerOnlyResolverRequest: CustomResolver = (
context: CustomResolutionContext,
moduleName: string,
platform: string | null,
oldMetroModuleName?: string,
) => {
if (
(platform === 'android' || platform === 'ios') &&
SENTRY_CORE_SERVER_ONLY_MODULE_RE.test(oldMetroModuleName ?? moduleName)
) {
return { type: 'empty' } as Resolution;
}
if (originalResolver) {
return oldMetroModuleName
? originalResolver(context, moduleName, platform, oldMetroModuleName)
: originalResolver(context, moduleName, platform);
}

if (context.resolveRequest === sentryServerOnlyResolverRequest) {
return context.resolveRequest(context, moduleName, platform);
}

return context.resolveRequest(context, moduleName, platform);
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
};

return {
...config,
resolver: {
...config.resolver,
resolveRequest: sentryServerOnlyResolverRequest,
},
};
}

type MetroFrame = Parameters<Required<Required<MetroConfig>['symbolicator']>['customizeFrame']>[0];
type MetroCustomizeFrame = { readonly collapse?: boolean };
type MetroCustomizeFrameReturnValue =
Expand Down
78 changes: 78 additions & 0 deletions packages/core/test/tools/metroconfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SentryExpoConfigOptions } from '../../src/js/tools/metroconfig';
import {
getSentryExpoConfig,
withSentryBabelTransformer,
withSentryExcludeServerOnlyResolver,
withSentryFramesCollapsed,
withSentryResolver,
} from '../../src/js/tools/metroconfig';
Expand Down Expand Up @@ -362,6 +363,83 @@ describe('metroconfig', () => {
}
});
});
describe('withSentryExcludeServerOnlyResolver', () => {
let originalResolverMock: any;

// @ts-expect-error Can't see type CustomResolutionContext
let contextMock: CustomResolutionContext;
let config: MetroConfig = {};

beforeEach(() => {
originalResolverMock = jest.fn();
contextMock = {
resolveRequest: jest.fn(),
};

config = {
resolver: {
resolveRequest: originalResolverMock,
},
};
});

describe.each([
['@sentry/core/build/esm/integrations/mcp-server/index.js'],
['@sentry/core/build/esm/tracing/openai/index.js'],
['@sentry/core/build/esm/tracing/anthropic-ai/index.js'],
['@sentry/core/build/esm/tracing/google-genai/index.js'],
['@sentry/core/build/esm/tracing/vercel-ai/index.js'],
['@sentry/core/build/esm/tracing/langchain/index.js'],
['@sentry/core/build/esm/tracing/langgraph/index.js'],
['@sentry/core/build/esm/utils/ai/providerSkip.js'],
['@sentry/core/build/cjs/integrations/mcp-server/index.js'],
['@sentry/core/build/cjs/tracing/openai/index.js'],
])('with server-only module %s', serverOnlyModule => {
test('removes module when platform is android', () => {
const modifiedConfig = withSentryExcludeServerOnlyResolver(config);
const result = modifiedConfig.resolver?.resolveRequest?.(contextMock, serverOnlyModule, 'android');

expect(result).toEqual({ type: 'empty' });
expect(originalResolverMock).not.toHaveBeenCalled();
});

test('removes module when platform is ios', () => {
const modifiedConfig = withSentryExcludeServerOnlyResolver(config);
const result = modifiedConfig.resolver?.resolveRequest?.(contextMock, serverOnlyModule, 'ios');

expect(result).toEqual({ type: 'empty' });
expect(originalResolverMock).not.toHaveBeenCalled();
});

test('keeps module when platform is web', () => {
const modifiedConfig = withSentryExcludeServerOnlyResolver(config);
modifiedConfig.resolver?.resolveRequest?.(contextMock, serverOnlyModule, 'web');

expect(originalResolverMock).toHaveBeenCalledWith(contextMock, serverOnlyModule, 'web');
});

test('keeps module when platform is null', () => {
const modifiedConfig = withSentryExcludeServerOnlyResolver(config);
modifiedConfig.resolver?.resolveRequest?.(contextMock, serverOnlyModule, null);

expect(originalResolverMock).toHaveBeenCalledWith(contextMock, serverOnlyModule, null);
});
});

test('calls originalResolver for non-AI modules on native platforms', () => {
const modifiedConfig = withSentryExcludeServerOnlyResolver(config);
modifiedConfig.resolver?.resolveRequest?.(contextMock, 'some/other/module', 'android');

expect(originalResolverMock).toHaveBeenCalledWith(contextMock, 'some/other/module', 'android');
});

test('falls back to context.resolveRequest when no originalResolver', () => {
const modifiedConfig = withSentryExcludeServerOnlyResolver({ resolver: {} });
modifiedConfig.resolver?.resolveRequest?.(contextMock, 'some/other/module', 'android');

expect(contextMock.resolveRequest).toHaveBeenCalledWith(contextMock, 'some/other/module', 'android');
});
});
});

// function create mock metro frame
Expand Down
Loading