UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

503 lines 18.6 kB
"use strict"; 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 }); exports.checkIfRunsOnProdMode = exports.checkIfRunsOnDevMode = exports.checkIfExpoBundles = exports.checkIfReactNativeBundles = exports.checkIfFlutterBuilds = exports.checkIfLints = exports.checkIfBuilds = exports.checkSentryProperties = exports.checkEnvBuildPlugin = exports.checkSentryCliRc = exports.checkPackageJson = exports.checkFileDoesNotExist = exports.checkFileExists = exports.checkFileContents = exports.checkFileDoesNotContain = exports.modifyFile = exports.createFile = exports.getWizardCommand = exports.ProcessRunner = exports.createIsolatedTestEnv = exports.log = exports.TEST_ARGS = exports.KEYS = void 0; const fs = __importStar(require("node:fs")); const os = __importStar(require("node:os")); const path = __importStar(require("node:path")); const node_child_process_1 = require("node:child_process"); const Logging_1 = require("../../lib/Helper/Logging"); const vitest_1 = require("vitest"); exports.KEYS = { UP: '\u001b[A', DOWN: '\u001b[B', LEFT: '\u001b[D', RIGHT: '\u001b[C', ENTER: '\r', SPACE: ' ', }; exports.TEST_ARGS = { AUTH_TOKEN: process.env.SENTRY_TEST_AUTH_TOKEN || 'TEST_AUTH_TOKEN', PROJECT_DSN: process.env.SENTRY_TEST_DSN || 'https://public@dsn.ingest.sentry.io/1337', ORG_SLUG: process.env.SENTRY_TEST_ORG || 'TEST_ORG_SLUG', PROJECT_SLUG: process.env.SENTRY_TEST_PROJECT || 'TEST_PROJECT_SLUG', }; exports.log = { success: (message) => { (0, Logging_1.green)(`[SUCCESS] ${message}`); }, info: (message) => { (0, Logging_1.dim)(`[INFO] ${message}`); }, error: (message) => { function formatMessage(message, depth) { if (depth > 3) { return '...'; } if (message instanceof Error) { return JSON.stringify({ name: message.name, message: message.message, stack: message.stack, ...(message.cause ? { cause: formatMessage(message.cause, depth + 1), } : {}), }, null, 2); } return String(message); } (0, Logging_1.red)(`[ERROR] ${formatMessage(message, 0)}`); }, }; /** * Creates an isolated test environment by copying a test application to a temporary directory. * Each call creates a NEW unique temporary directory, allowing multiple isolated environments * per test file (useful for tests that run the wizard multiple times with different configs). * * @param testAppName - Name of the test application folder (e.g., 'nextjs-16-test-app') * @returns Object with projectDir path and cleanup function */ function createIsolatedTestEnv(testAppName) { const sourceDir = path.resolve(__dirname, '../test-applications', testAppName); const tmpBaseDir = path.join(os.tmpdir(), 'sentry-wizard-e2e'); if (!fs.existsSync(tmpBaseDir)) { fs.mkdirSync(tmpBaseDir, { recursive: true }); } const projectDir = fs.mkdtempSync(path.join(tmpBaseDir, `${testAppName}-`)); exports.log.info(`Created isolated test env at: ${projectDir}`); try { fs.cpSync(sourceDir, projectDir, { recursive: true }); } catch (e) { exports.log.error('Error copying test application'); exports.log.error(e); throw e; } initGit(projectDir); const cleanup = () => { try { const keepOnFailure = process.env.SENTRY_WIZARD_E2E_KEEP_TEMP === 'true'; if (keepOnFailure) { exports.log.info(`Keeping temp directory for debugging: ${projectDir}`); } else { fs.rmSync(projectDir, { recursive: true, force: true }); exports.log.info(`Cleaned up isolated test env: ${projectDir}`); } } catch (e) { exports.log.error(`Error cleaning up test environment at ${projectDir}`); exports.log.error(e); } }; return { projectDir, cleanup }; } exports.createIsolatedTestEnv = createIsolatedTestEnv; class ProcessRunner { taskHandle; constructor(cmd, args, opts) { this.taskHandle = (0, node_child_process_1.spawn)(cmd, args, { cwd: opts?.cwd, stdio: 'pipe' }); if (opts?.debug) { this.taskHandle.stdout?.pipe(process.stdout); this.taskHandle.stderr?.pipe(process.stderr); } } /** * Waits for the task to exit with a given `statusCode`. * * @returns a promise that resolves to `true` if the run ends with the status * code, or it rejects when the `timeout` was reached. */ waitForStatusCode(statusCode, options = {}) { const { timeout } = { timeout: 60000, ...options, }; return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.kill(); reject(new Error(`Timeout waiting for status code: ${statusCode ?? 'null'}`)); }, timeout); this.taskHandle.on('error', (err) => { clearTimeout(timeoutId); reject(err); }); this.taskHandle.on('exit', (code) => { clearTimeout(timeoutId); resolve(code === statusCode); }); }); } /** * Waits for the provided output with `.includes()` logic. * * @returns a promise that resolves to `true` if the output was found, `false` if the output was not found within the * timeout and `optional: true` is set, or it rejects when the timeout was reached with `optional: false` */ waitForOutput(output, options = {}) { const { timeout, optional } = { timeout: 60000, optional: false, ...options, }; return new Promise((resolve, reject) => { let outputBuffer = ''; const timeoutId = setTimeout(() => { this.taskHandle.off('error', errorListener); this.taskHandle.stdout?.off('data', dataListener); this.kill(); if (optional) { // The output is not found but it's optional so we can resolve the promise with false resolve(false); } else { reject(new Error(`Timeout waiting for output: ${output}. Got the following instead: ${outputBuffer}`)); } }, timeout); const dataListener = (data) => { outputBuffer += data; if (outputBuffer.includes(output)) { clearTimeout(timeoutId); this.taskHandle.off('error', errorListener); this.taskHandle.stdout?.off('data', dataListener); // The output is found so we can resolve the promise with true resolve(true); } }; const errorListener = (err) => { this.taskHandle.off('error', errorListener); this.taskHandle.stdout?.off('data', dataListener); clearTimeout(timeoutId); reject(err); }; this.taskHandle.on('error', errorListener); this.taskHandle.stdout?.on('data', dataListener); }); } kill() { this.taskHandle.stdin?.destroy(); this.taskHandle.stderr?.destroy(); this.taskHandle.stdout?.destroy(); this.taskHandle.kill('SIGINT'); this.taskHandle.unref(); } } exports.ProcessRunner = ProcessRunner; function getWizardCommand(integration) { const binName = process.env.SENTRY_WIZARD_E2E_TEST_BIN ? ['dist-bin', `sentry-wizard-${process.platform}-${process.arch}`] : ['dist', 'bin.js']; const binPath = path.join(__dirname, '..', '..', ...binName); const args = [ '--debug', '-i', integration, '--preSelectedProject.authToken', exports.TEST_ARGS.AUTH_TOKEN, '--preSelectedProject.dsn', exports.TEST_ARGS.PROJECT_DSN, '--preSelectedProject.orgSlug', exports.TEST_ARGS.ORG_SLUG, '--preSelectedProject.projectSlug', exports.TEST_ARGS.PROJECT_SLUG, '--disable-telemetry', ]; return `${binPath} ${args.join(' ')}`; } exports.getWizardCommand = getWizardCommand; /** * Create a file with the given content * * @param filePath * @param content */ function createFile(filePath, content) { return fs.writeFileSync(filePath, content || ''); } exports.createFile = createFile; /** * Modify the file with the new content * * @param filePath * @param oldContent * @param newContent */ function modifyFile(filePath, replaceMap) { const fileContent = fs.readFileSync(filePath, 'utf-8'); let newFileContent = fileContent; for (const [oldContent, newContent] of Object.entries(replaceMap)) { newFileContent = newFileContent.replace(oldContent, newContent); } fs.writeFileSync(filePath, newFileContent); } exports.modifyFile = modifyFile; /** * Read the file contents and check if it does not contain the given content * * @param {string} filePath * @param {(string | string[])} content */ function checkFileDoesNotContain(filePath, content) { const fileContent = fs.readFileSync(filePath, 'utf-8'); const contentArray = Array.isArray(content) ? content : [content]; for (const c of contentArray) { (0, vitest_1.expect)(fileContent).not.toContain(c); } } exports.checkFileDoesNotContain = checkFileDoesNotContain; /** * Read the file contents and check if it contains the given content * * @param {string} filePath * @param {(string | string[])} content */ function checkFileContents(filePath, content) { const fileContent = fs.readFileSync(filePath, 'utf-8'); const contentArray = Array.isArray(content) ? content : [content]; for (const c of contentArray) { (0, vitest_1.expect)(fileContent).toContain(c); } } exports.checkFileContents = checkFileContents; /** * Check if the file exists * * @param filePath */ function checkFileExists(filePath) { (0, vitest_1.expect)(fs.existsSync(filePath)).toBe(true); } exports.checkFileExists = checkFileExists; /** * Check if the file does not exist * * @param filePath */ function checkFileDoesNotExist(filePath) { (0, vitest_1.expect)(fs.existsSync(filePath)).toBe(false); } exports.checkFileDoesNotExist = checkFileDoesNotExist; /** * Check if the package.json lists the given package as a dependency or dev dependency * * @param projectDir * @param integration */ function checkPackageJson(projectDir, packageName, devDependency = false) { const packageJson = fs.readFileSync(`${projectDir}/package.json`, 'utf-8'); const packageJsonObject = JSON.parse(packageJson); const packageVersion = packageJsonObject.dependencies?.[packageName] || (devDependency && packageJsonObject.devDependencies?.[packageName]); (0, vitest_1.expect)(packageVersion).toBeTruthy(); } exports.checkPackageJson = checkPackageJson; /** * Check if the .sentryclirc contains the auth token * * @param projectDir */ function checkSentryCliRc(projectDir) { checkFileContents(`${projectDir}/.sentryclirc`, `token=${exports.TEST_ARGS.AUTH_TOKEN}`); } exports.checkSentryCliRc = checkSentryCliRc; /** * Check if the .env.sentry-build-plugin contains the auth token * @param projectDir */ function checkEnvBuildPlugin(projectDir) { checkFileContents(`${projectDir}/.env.sentry-build-plugin`, `SENTRY_AUTH_TOKEN=${exports.TEST_ARGS.AUTH_TOKEN}`); } exports.checkEnvBuildPlugin = checkEnvBuildPlugin; /** * Check if the sentry.properties contains the auth token * @param projectDir */ function checkSentryProperties(projectDir) { checkFileContents(`${projectDir}/sentry.properties`, `auth_token=${exports.TEST_ARGS.AUTH_TOKEN}`); } exports.checkSentryProperties = checkSentryProperties; /** * Check if the project builds * Check if the project builds and ends with status code 0. * @param projectDir */ async function checkIfBuilds(projectDir) { const npmRunner = new ProcessRunner('npm', ['run', 'build'], { cwd: projectDir, }); const builtSuccessfully = await npmRunner.waitForStatusCode(0, { timeout: 120000, }); (0, vitest_1.expect)(builtSuccessfully).toBe(true); } exports.checkIfBuilds = checkIfBuilds; /** * Check if the project lints successfully * Runs `npm run lint` and expects status code 0. * @param projectDir */ async function checkIfLints(projectDir) { const npmRunner = new ProcessRunner('npm', ['run', 'lint'], { cwd: projectDir, }); const lintedSuccessfully = await npmRunner.waitForStatusCode(0, { timeout: 120000, }); (0, vitest_1.expect)(lintedSuccessfully).toBe(true); } exports.checkIfLints = checkIfLints; /** * Check if the flutter project builds * @param projectDir */ async function checkIfFlutterBuilds(projectDir, expectedOutput, debug = false) { const flutterRunner = new ProcessRunner('flutter', ['build', 'web'], { cwd: projectDir, debug: debug, }); const outputReceived = await flutterRunner.waitForOutput(expectedOutput, { timeout: 120000, }); (0, vitest_1.expect)(outputReceived).toBe(true); } exports.checkIfFlutterBuilds = checkIfFlutterBuilds; /** * Check if the React Native project bundles successfully for the specified platform. * Returns a boolean indicating if the process exits with status code 0. * @param projectDir The root directory of the React Native project. * @param platform The platform to bundle for ('ios' or 'android'). * @param debug runs the command in debug mode if true */ async function checkIfReactNativeBundles(projectDir, platform, debug = false) { const entryFile = 'index.js'; const dev = 'false'; // Test a production-like bundle let bundleOutput; let assetsDest; if (platform === 'ios') { bundleOutput = './ios/main.jsbundle'; assetsDest = './ios'; } else { // android bundleOutput = './android/app/src/main/assets/index.android.bundle'; assetsDest = './android/app/src/main/res'; } const bundleCommandArgs = [ 'react-native', 'bundle', '--entry-file', entryFile, '--platform', platform, '--dev', dev, '--bundle-output', bundleOutput, '--assets-dest', assetsDest, ]; const npxRunner = new ProcessRunner('npx', bundleCommandArgs, { cwd: projectDir, debug: debug, }); const builtSuccessfully = await npxRunner.waitForStatusCode(0, { timeout: 300000, }); npxRunner.kill(); return builtSuccessfully; } exports.checkIfReactNativeBundles = checkIfReactNativeBundles; /** * Check if the Expo project exports successfully for the specified platform. * Returns a boolean indicating if the process exits with status code 0. * @param projectDir The root directory of the Expo project. * @param platform The platform to export for ('ios', 'android', or 'web'). * @param debug runs the command in debug mode if true */ async function checkIfExpoBundles(projectDir, platform, debug = false) { const exportCommandArgs = ['expo', 'export', '--platform', platform]; const npxRunner = new ProcessRunner('npx', exportCommandArgs, { cwd: projectDir, debug: debug, }); const builtSuccessfully = await npxRunner.waitForStatusCode(0, { timeout: 300000, }); npxRunner.kill(); return builtSuccessfully; } exports.checkIfExpoBundles = checkIfExpoBundles; /** * Check if the project runs on dev mode * @param projectDir * @param expectedOutput */ async function checkIfRunsOnDevMode(projectDir, expectedOutput) { const npmRunner = new ProcessRunner('npm', ['run', 'dev'], { cwd: projectDir, }); (0, vitest_1.expect)(await npmRunner.waitForOutput(expectedOutput, { timeout: 120000, })).toBe(true); npmRunner.kill(); } exports.checkIfRunsOnDevMode = checkIfRunsOnDevMode; /** * Check if the project runs on prod mode * @param projectDir * @param expectedOutput */ async function checkIfRunsOnProdMode(projectDir, expectedOutput, startCommand = 'start') { const npmRunner = new ProcessRunner('npm', ['run', startCommand], { cwd: projectDir, }); (0, vitest_1.expect)(await npmRunner.waitForOutput(expectedOutput, { timeout: 120000, })).toBe(true); npmRunner.kill(); } exports.checkIfRunsOnProdMode = checkIfRunsOnProdMode; /** * Initialize a git repository in the given directory * @param projectDir */ function initGit(projectDir) { try { (0, node_child_process_1.execSync)('git init', { cwd: projectDir }); // Add all files to the git repo (0, node_child_process_1.execSync)('git add -A', { cwd: projectDir }); // Add author info to avoid git commit error (0, node_child_process_1.execSync)('git config user.email test@test.sentry.io', { cwd: projectDir }); (0, node_child_process_1.execSync)('git config user.name Test', { cwd: projectDir }); (0, node_child_process_1.execSync)('git commit -m init', { cwd: projectDir }); } catch (e) { exports.log.error('Error initializing git'); exports.log.error(e); } } //# sourceMappingURL=index.js.map