@sentry/wizard
Version:
Sentry wizard helping you to configure your project
503 lines • 18.6 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 });
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