@sentry/wizard
Version:
Sentry wizard helping you to configure your project
275 lines (273 loc) • 13.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = __importStar(require("node:path"));
const fs = __importStar(require("node:fs"));
const Constants_1 = require("../../lib/Constants");
const utils_1 = require("../utils");
const vitest_1 = require("vitest");
//@ts-expect-error - clifty is ESM only
const clifty_1 = require("clifty");
async function runWizardOnReactRouterProject(projectDir, opts) {
const { modifiedFiles = false } = opts || {};
const wizardInteraction = (0, clifty_1.withEnv)({
cwd: projectDir,
}).defineInteraction();
if (modifiedFiles) {
wizardInteraction
.whenAsked('Do you want to continue anyway?')
.respondWith(clifty_1.KEYS.ENTER);
}
wizardInteraction
.whenAsked('Please select your package manager.')
.respondWith(clifty_1.KEYS.DOWN, clifty_1.KEYS.ENTER)
.expectOutput('Installing @sentry/react-router')
.expectOutput('Installed @sentry/react-router', {
timeout: 240000,
})
.whenAsked('Do you want to enable Tracing')
.respondWith(clifty_1.KEYS.ENTER)
.whenAsked('Do you want to enable Session Replay')
.respondWith(clifty_1.KEYS.ENTER)
.whenAsked('Do you want to enable Logs')
.respondWith(clifty_1.KEYS.ENTER)
.whenAsked('Do you want to enable Profiling')
.respondWith(clifty_1.KEYS.ENTER)
.whenAsked('Do you want to use the Instrumentation API')
.respondWith(clifty_1.KEYS.DOWN, clifty_1.KEYS.ENTER) // No
.expectOutput('Installing @sentry/profiling-node')
.expectOutput('Installed @sentry/profiling-node', {
timeout: 240000,
})
.whenAsked('Do you want to create an example page')
.respondWith(clifty_1.KEYS.ENTER);
if (modifiedFiles) {
wizardInteraction
.whenAsked('Would you like to try running npx react-router reveal')
.respondWith(clifty_1.KEYS.ENTER)
.whenAsked('Did you apply the snippet above?')
.respondWith(clifty_1.KEYS.ENTER);
}
return wizardInteraction
.whenAsked('Optionally add a project-scoped MCP server configuration for the Sentry MCP?')
.respondWith(clifty_1.KEYS.DOWN, clifty_1.KEYS.ENTER)
.expectOutput('Successfully installed the Sentry React Router SDK!')
.run((0, utils_1.getWizardCommand)(Constants_1.Integration.reactRouter));
}
(0, vitest_1.describe)('React Router', () => {
(0, vitest_1.describe)('with empty project', () => {
let wizardExitCode;
const { projectDir, cleanup } = (0, utils_1.createIsolatedTestEnv)('react-router-test-app');
(0, vitest_1.beforeAll)(async () => {
wizardExitCode = await runWizardOnReactRouterProject(projectDir);
});
(0, vitest_1.afterAll)(() => {
cleanup();
});
(0, vitest_1.test)('exits with exit code 0', () => {
(0, vitest_1.expect)(wizardExitCode).toBe(0);
});
(0, vitest_1.test)('package.json is updated correctly', () => {
(0, utils_1.checkPackageJson)(projectDir, '@sentry/react-router');
});
(0, vitest_1.test)('.env.sentry-build-plugin is created and contains the auth token', () => {
(0, utils_1.checkEnvBuildPlugin)(projectDir);
});
(0, vitest_1.test)('example page exists', () => {
(0, utils_1.checkFileExists)(`${projectDir}/app/routes/sentry-example-page.tsx`);
});
(0, vitest_1.test)('example API route exists', () => {
(0, utils_1.checkFileExists)(`${projectDir}/app/routes/api.sentry-example-api.ts`);
});
(0, vitest_1.test)('example page is added to routes configuration', () => {
(0, utils_1.checkFileContents)(`${projectDir}/app/routes.ts`, [
'route("/sentry-example-page", "routes/sentry-example-page.tsx")',
'route("/api/sentry-example-api", "routes/api.sentry-example-api.ts")',
]);
});
(0, vitest_1.test)('instrument.server file exists', () => {
(0, utils_1.checkFileExists)(`${projectDir}/instrument.server.mjs`);
});
(0, vitest_1.test)('entry.client file contains Sentry initialization', () => {
(0, utils_1.checkFileContents)(`${projectDir}/app/entry.client.tsx`, [
'import * as Sentry from',
'@sentry/react-router',
`Sentry.init({
dsn: "${utils_1.TEST_ARGS.PROJECT_DSN}",`,
'integrations: [Sentry.reactRouterTracingIntegration(), Sentry.replayIntegration()]',
'enableLogs: true,',
'tracesSampleRate: 1.0,',
]);
});
(0, vitest_1.test)('package.json scripts are updated correctly', () => {
(0, utils_1.checkFileContents)(`${projectDir}/package.json`, [
`"start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js"`,
`"dev": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router dev"`,
]);
});
(0, vitest_1.test)('entry.server file contains Sentry instrumentation', () => {
(0, utils_1.checkFileContents)(`${projectDir}/app/entry.server.tsx`, [
'import * as Sentry from',
'@sentry/react-router',
'export const handleError = Sentry.createSentryHandleError(',
'export default Sentry.wrapSentryHandleRequest(handleRequest);',
]);
});
(0, vitest_1.test)('instrument.server file contains Sentry initialization', () => {
(0, utils_1.checkFileContents)(`${projectDir}/instrument.server.mjs`, [
"import * as Sentry from '@sentry/react-router';",
`Sentry.init({
dsn: "${utils_1.TEST_ARGS.PROJECT_DSN}",`,
'enableLogs: true,',
]);
});
(0, vitest_1.test)('root file contains Sentry ErrorBoundary', () => {
(0, utils_1.checkFileContents)(`${projectDir}/app/root.tsx`, [
'import * as Sentry from',
'@sentry/react-router',
'export function ErrorBoundary',
'Sentry.captureException(error)',
]);
});
(0, vitest_1.test)('vite.config file contains sentryReactRouter plugin', () => {
(0, utils_1.checkFileContents)(`${projectDir}/vite.config.ts`, [
'import { sentryReactRouter } from',
'@sentry/react-router',
'sentryReactRouter(',
'authToken: process.env.SENTRY_AUTH_TOKEN',
]);
});
(0, vitest_1.test)('react-router.config file contains buildEnd hook with sentryOnBuildEnd', () => {
(0, utils_1.checkFileContents)(`${projectDir}/react-router.config.ts`, [
'import { sentryOnBuildEnd } from',
'@sentry/react-router',
'ssr: true,',
'buildEnd: async',
'await sentryOnBuildEnd({',
]);
});
(0, vitest_1.test)('builds successfully', async () => {
await (0, utils_1.checkIfBuilds)(projectDir);
}, 60000); // 1 minute timeout
(0, vitest_1.test)('runs on dev mode correctly', async () => {
await (0, utils_1.checkIfRunsOnDevMode)(projectDir, 'to expose');
}, 30000); // 30 second timeout
(0, vitest_1.test)('runs on prod mode correctly', async () => {
await (0, utils_1.checkIfRunsOnProdMode)(projectDir, 'react-router-serve');
}, 30000); // 30 second timeout
});
(0, vitest_1.describe)('edge cases', () => {
(0, vitest_1.describe)('existing Sentry setup', () => {
let wizardExitCode;
const { projectDir, cleanup } = (0, utils_1.createIsolatedTestEnv)('react-router-test-app');
(0, vitest_1.beforeAll)(async () => {
// Add existing Sentry setup to the isolated test app
const clientEntryPath = path.join(projectDir, 'app', 'entry.client.tsx');
const existingContent = `import * as Sentry from "@sentry/react-router";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
Sentry.init({
dsn: "https://existing@dsn.ingest.sentry.io/1337",
tracesSampleRate: 1.0,
});
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});`;
fs.writeFileSync(clientEntryPath, existingContent);
wizardExitCode = await runWizardOnReactRouterProject(projectDir, {
modifiedFiles: true,
});
});
(0, vitest_1.afterAll)(() => {
cleanup();
});
(0, vitest_1.test)('exits with exit code 0', () => {
(0, vitest_1.expect)(wizardExitCode).toBe(0);
});
(0, vitest_1.test)('wizard handles existing Sentry without duplication', () => {
const clientContent = fs.readFileSync(`${projectDir}/app/entry.client.tsx`, 'utf8');
const sentryImportCount = (clientContent.match(/import \* as Sentry from "@sentry\/react-router"/g) || []).length;
const sentryInitCount = (clientContent.match(/Sentry\.init\(/g) || [])
.length;
(0, vitest_1.expect)(sentryImportCount).toBe(1);
(0, vitest_1.expect)(sentryInitCount).toBe(1);
});
// Only test the essential checks for this edge case
(0, vitest_1.test)('package.json is updated correctly', () => {
(0, utils_1.checkPackageJson)(projectDir, '@sentry/react-router');
});
(0, vitest_1.test)('essential files exist or wizard completes gracefully', () => {
// Check if key directories exist
(0, vitest_1.expect)(fs.existsSync(`${projectDir}/app`)).toBe(true);
// When there's existing Sentry setup, the wizard may skip some file creation
// to avoid conflicts. This is acceptable behavior.
// Let's check if the wizard at least completed by verifying package.json was updated
const packageJsonPath = `${projectDir}/package.json`;
(0, vitest_1.expect)(fs.existsSync(packageJsonPath)).toBe(true);
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
const hasSentryPackage = packageJson.dependencies?.['@sentry/react-router'] ||
packageJson.devDependencies?.['@sentry/react-router'];
// The wizard should have at least installed the Sentry package
(0, vitest_1.expect)(hasSentryPackage).toBeTruthy();
});
});
(0, vitest_1.describe)('missing entry files', () => {
let wizardExitCode;
const { projectDir, cleanup } = (0, utils_1.createIsolatedTestEnv)('react-router-test-app');
(0, vitest_1.beforeAll)(async () => {
// Copy project and remove entry files
const entryClientPath = path.join(projectDir, 'app', 'entry.client.tsx');
const entryServerPath = path.join(projectDir, 'app', 'entry.server.tsx');
if (fs.existsSync(entryClientPath))
fs.unlinkSync(entryClientPath);
if (fs.existsSync(entryServerPath))
fs.unlinkSync(entryServerPath);
wizardExitCode = await runWizardOnReactRouterProject(projectDir);
});
(0, vitest_1.afterAll)(() => {
cleanup();
});
(0, vitest_1.test)('exits with exit code 0', () => {
(0, vitest_1.expect)(wizardExitCode).toBe(0);
});
(0, vitest_1.test)('wizard creates missing entry files', () => {
(0, utils_1.checkFileExists)(`${projectDir}/app/entry.client.tsx`);
(0, utils_1.checkFileExists)(`${projectDir}/app/entry.server.tsx`);
});
(0, vitest_1.test)('basic configuration still works', () => {
(0, utils_1.checkPackageJson)(projectDir, '@sentry/react-router');
(0, utils_1.checkFileExists)(`${projectDir}/instrument.server.mjs`);
});
});
});
});
//# sourceMappingURL=react-router.test.js.map