diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/featureFlag/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/featureFlag/page.tsx new file mode 100644 index 000000000000..3db71ae022ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/featureFlag/page.tsx @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/nextjs'; + +export const dynamic = 'force-dynamic'; + +export default async function FeatureFlagServerComponent() { + Sentry.buildLaunchDarklyFlagUsedHandler(); + Sentry.launchDarklyIntegration(); + Sentry.openFeatureIntegration(); + new Sentry.OpenFeatureIntegrationHook(); + // @ts-ignore - we just want to test that the statsigIntegration is imported + Sentry.statsigIntegration(); + // @ts-ignore - we just want to test that the unleashIntegration is imported + Sentry.unleashIntegration(); + + return
FeatureFlagServerComponent
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 0a32972b0e6a..52f6ae13875a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -133,3 +133,9 @@ test('Should capture an error and transaction for a app router page', async ({ p }), ); }); + +test('Should not throw error on server component when importing shimmed feature flag function', async ({ page }) => { + await page.goto('/server-component/featureFlag'); + // tests that none of the feature flag functions throw an error when imported in a node environment + await expect(page.locator('body')).toContainText('FeatureFlagServerComponent'); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 83a135e71f21..90ea06e0ef07 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -138,6 +138,12 @@ export { NODE_VERSION, featureFlagsIntegration, type FeatureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index d74a885c2b37..ceb4fc6d8a51 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -29,4 +29,11 @@ export declare const Span: clientSdk.Span; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; + export default sentryAstro; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index f64ee53dc47c..f7a5f77ac0fb 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -124,6 +124,12 @@ export { NODE_VERSION, featureFlagsIntegration, type FeatureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 4a9d7fd9d71c..38bd74fda8c2 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -142,6 +142,12 @@ export { createSentryWinstonTransport, wrapMcpServerWithSentry, featureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index f0bed369acee..14797a9af008 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -124,6 +124,12 @@ export { NODE_VERSION, featureFlagsIntegration, type FeatureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts index 510b26ddbb76..e887ac725023 100644 --- a/packages/integration-shims/src/index.ts +++ b/packages/integration-shims/src/index.ts @@ -1,3 +1,4 @@ export { feedbackIntegrationShim } from './Feedback'; export { replayIntegrationShim } from './Replay'; export { browserTracingIntegrationShim } from './BrowserTracing'; +export { launchDarklyIntegrationShim, buildLaunchDarklyFlagUsedHandlerShim } from './launchDarkly'; diff --git a/packages/integration-shims/src/launchDarkly.ts b/packages/integration-shims/src/launchDarkly.ts new file mode 100644 index 000000000000..76750f5c863c --- /dev/null +++ b/packages/integration-shims/src/launchDarkly.ts @@ -0,0 +1,38 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; +import { FAKE_FUNCTION } from './common'; + +/** + * This is a shim for the LaunchDarkly integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const launchDarklyIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The launchDarklyIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'LaunchDarkly', + }; +}); + +/** + * This is a shim for the LaunchDarkly flag used handler. + */ +export function buildLaunchDarklyFlagUsedHandlerShim(): unknown { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The buildLaunchDarklyFlagUsedHandler() can only be used in the browser.'); + }); + } + + return { + name: 'sentry-flag-auditor', + type: 'flag-used', + synchronous: true, + method: FAKE_FUNCTION, + }; +} diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 04b73ea4c83e..fe5a75bd5c8b 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -141,3 +141,10 @@ export declare function wrapApiHandlerWithSentryVercelCrons(WrappingTarget: C): C; export { captureRequestError } from './common/captureRequestError'; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 1c02da9fff2e..71970174721c 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -37,6 +37,14 @@ export { amqplibIntegration } from './integrations/tracing/amqplib'; export { vercelAIIntegration } from './integrations/tracing/vercelai'; export { childProcessIntegration } from './integrations/childProcess'; export { createSentryWinstonTransport } from './integrations/winston'; +export { + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, +} from './integrations/featureFlagShims'; export { SentryContextManager } from './otel/contextManager'; export { generateInstrumentOnce } from './otel/instrument'; diff --git a/packages/node/src/integrations/featureFlagShims/index.ts b/packages/node/src/integrations/featureFlagShims/index.ts new file mode 100644 index 000000000000..230dbaeeb7e8 --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/index.ts @@ -0,0 +1,13 @@ +export { + launchDarklyIntegrationShim as launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandlerShim as buildLaunchDarklyFlagUsedHandler, +} from './launchDarkly'; + +export { + openFeatureIntegrationShim as openFeatureIntegration, + OpenFeatureIntegrationHookShim as OpenFeatureIntegrationHook, +} from './openFeature'; + +export { statsigIntegrationShim as statsigIntegration } from './statsig'; + +export { unleashIntegrationShim as unleashIntegration } from './unleash'; diff --git a/packages/node/src/integrations/featureFlagShims/launchDarkly.ts b/packages/node/src/integrations/featureFlagShims/launchDarkly.ts new file mode 100644 index 000000000000..c525e2f366ac --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/launchDarkly.ts @@ -0,0 +1,37 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the LaunchDarkly integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const launchDarklyIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The launchDarklyIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'LaunchDarkly', + }; +}); + +/** + * This is a shim for the LaunchDarkly flag used handler. + */ +export function buildLaunchDarklyFlagUsedHandlerShim(): unknown { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The buildLaunchDarklyFlagUsedHandler() can only be used in the browser.'); + }); + } + + return { + name: 'sentry-flag-auditor', + type: 'flag-used', + synchronous: true, + method: () => null, + }; +} diff --git a/packages/node/src/integrations/featureFlagShims/openFeature.ts b/packages/node/src/integrations/featureFlagShims/openFeature.ts new file mode 100644 index 000000000000..d0768fb618de --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/openFeature.ts @@ -0,0 +1,49 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the OpenFeature integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const openFeatureIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The openFeatureIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'OpenFeature', + }; +}); + +/** + * This is a shim for the OpenFeature integration hook. + */ +export class OpenFeatureIntegrationHookShim { + /** + * + */ + public constructor() { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The OpenFeatureIntegrationHook can only be used in the browser.'); + }); + } + } + + /** + * + */ + public after(): void { + // No-op + } + + /** + * + */ + public error(): void { + // No-op + } +} diff --git a/packages/node/src/integrations/featureFlagShims/statsig.ts b/packages/node/src/integrations/featureFlagShims/statsig.ts new file mode 100644 index 000000000000..8a74170d2b1c --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/statsig.ts @@ -0,0 +1,18 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the Statsig integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const statsigIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The statsigIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'Statsig', + }; +}); diff --git a/packages/node/src/integrations/featureFlagShims/unleash.ts b/packages/node/src/integrations/featureFlagShims/unleash.ts new file mode 100644 index 000000000000..748e63b71040 --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/unleash.ts @@ -0,0 +1,18 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the Unleash integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const unleashIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The unleashIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'Unleash', + }; +}); diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts index c6cdb01d280e..4f006e0b5b07 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -18,3 +18,10 @@ export declare const getDefaultIntegrations: (options: Options) => Integration[] export declare const defaultStackParser: StackParser; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/react-router/src/index.types.ts b/packages/react-router/src/index.types.ts index 45f4fe10fa31..150fc45a1e63 100644 --- a/packages/react-router/src/index.types.ts +++ b/packages/react-router/src/index.types.ts @@ -18,3 +18,10 @@ export declare const defaultStackParser: StackParser; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 697fc3813045..d0df7397f612 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -32,3 +32,10 @@ declare const runtime: 'client' | 'server'; export const close = runtime === 'client' ? clientSdk.close : serverSdk.close; export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush; export const lastEventId = runtime === 'client' ? clientSdk.lastEventId : serverSdk.lastEventId; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index e4cd974ed00e..7725d1ad3d3c 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -25,3 +25,10 @@ export declare function flush(timeout?: number | undefined): PromiseLike; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index 85bbe9df63fd..448ea35f637b 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -28,3 +28,10 @@ export declare const showReportDialog: typeof clientSdk.showReportDialog; export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration;