@sentry/wizard
Version:
Sentry wizard helping you to configure your project
305 lines • 15.9 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.instrumentSentryOnEntryServer = exports.updateStartScript = exports.initializeSentryOnEntryClient = exports.updateEntryClientMod = exports.updateBuildScript = exports.instrumentRootRoute = exports.loadRemixConfig = exports.isRemixV2 = exports.insertServerInstrumentationFile = exports.createServerInstrumentationFile = exports.generateServerInstrumentationFile = exports.runRemixReveal = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const url = __importStar(require("url"));
const childProcess = __importStar(require("child_process"));
// @ts-expect-error - clack is ESM and TS complains about that. It works though
const prompts_1 = __importDefault(require("@clack/prompts"));
const chalk_1 = __importDefault(require("chalk"));
const semver_1 = require("semver");
const magicast_1 = require("magicast");
const package_json_1 = require("../utils/package-json");
const utils_1 = require("./utils");
const root_1 = require("./codemods/root");
const handle_error_1 = require("./codemods/handle-error");
const clack_1 = require("../utils/clack");
const express_server_1 = require("./codemods/express-server");
const REMIX_CONFIG_FILE = 'remix.config.js';
const REMIX_REVEAL_COMMAND = 'npx remix reveal';
function runRemixReveal(isTS) {
// Check if entry files already exist
const clientEntryFilename = `entry.client.${isTS ? 'tsx' : 'jsx'}`;
const serverEntryFilename = `entry.server.${isTS ? 'tsx' : 'jsx'}`;
const clientEntryPath = path.join(process.cwd(), 'app', clientEntryFilename);
const serverEntryPath = path.join(process.cwd(), 'app', serverEntryFilename);
if (fs.existsSync(clientEntryPath) && fs.existsSync(serverEntryPath)) {
prompts_1.default.log.info(`Found entry files ${chalk_1.default.cyan(clientEntryFilename)} and ${chalk_1.default.cyan(serverEntryFilename)}.`);
}
else {
prompts_1.default.log.info(`Couldn't find entry files in your project. Trying to run ${chalk_1.default.cyan(REMIX_REVEAL_COMMAND)}...`);
prompts_1.default.log.info(childProcess.execSync(REMIX_REVEAL_COMMAND).toString());
}
}
exports.runRemixReveal = runRemixReveal;
function getInitCallArgs(dsn, type, selectedFeatures) {
const initCallArgs = {
dsn,
};
// Adding tracing sample rate for both client and server
if (selectedFeatures.performance) {
initCallArgs.tracesSampleRate = 1.0;
}
// Adding logs for both client and server
if (selectedFeatures.logs) {
initCallArgs.enableLogs = true;
}
// Adding integrations and replay options only for client
if (type === 'client' &&
(selectedFeatures.performance || selectedFeatures.replay)) {
initCallArgs.integrations = [];
if (selectedFeatures.performance) {
initCallArgs.integrations.push(magicast_1.builders.functionCall('browserTracingIntegration', magicast_1.builders.raw('{ useEffect, useLocation, useMatches }')));
}
if (selectedFeatures.replay) {
initCallArgs.integrations.push(magicast_1.builders.functionCall('replayIntegration', {
maskAllText: true,
blockAllMedia: true,
}));
initCallArgs.replaysSessionSampleRate = 0.1;
initCallArgs.replaysOnErrorSampleRate = 1.0;
}
}
return initCallArgs;
}
function insertClientInitCall(dsn,
// MagicAst returns `ProxifiedModule<any>` so therefore we have to use `any` here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalHooksMod, selectedFeatures) {
const initCallArgs = getInitCallArgs(dsn, 'client', selectedFeatures);
const initCall = magicast_1.builders.functionCall('init', initCallArgs);
const originalHooksModAST = originalHooksMod.$ast;
const initCallInsertionIndex = (0, utils_1.getAfterImportsInsertionIndex)(originalHooksModAST);
originalHooksModAST.body.splice(initCallInsertionIndex, 0,
// @ts-expect-error - string works here because the AST is proxified by magicast
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
(0, magicast_1.generateCode)(initCall).code);
}
function generateServerInstrumentationFile(dsn, selectedFeatures) {
// create an empty file named `instrument.server.mjs`
const instrumentationFile = 'instrumentation.server.mjs';
const instrumentationFileMod = (0, magicast_1.parseModule)('');
instrumentationFileMod.imports.$add({
from: '@sentry/remix',
imported: '*',
local: 'Sentry',
});
const initCallArgs = getInitCallArgs(dsn, 'server', selectedFeatures);
const initCall = magicast_1.builders.functionCall('Sentry.init', initCallArgs);
const instrumentationFileModAST = instrumentationFileMod.$ast;
const initCallInsertionIndex = (0, utils_1.getAfterImportsInsertionIndex)(instrumentationFileModAST);
instrumentationFileModAST.body.splice(initCallInsertionIndex, 0,
// @ts-expect-error - string works here because the AST is proxified by magicast
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
(0, magicast_1.generateCode)(initCall).code);
return { instrumentationFile, instrumentationFileMod };
}
exports.generateServerInstrumentationFile = generateServerInstrumentationFile;
async function createServerInstrumentationFile(dsn, selectedFeatures) {
const { instrumentationFile, instrumentationFileMod } = generateServerInstrumentationFile(dsn, selectedFeatures);
await (0, magicast_1.writeFile)(instrumentationFileMod.$ast, instrumentationFile);
return instrumentationFile;
}
exports.createServerInstrumentationFile = createServerInstrumentationFile;
async function insertServerInstrumentationFile(dsn, selectedFeatures) {
const instrumentationFile = await createServerInstrumentationFile(dsn, selectedFeatures);
const expressServerPath = await (0, express_server_1.findCustomExpressServerImplementation)();
if (!expressServerPath) {
return false;
}
const originalExpressServerMod = await (0, magicast_1.loadFile)(expressServerPath);
if ((0, utils_1.serverHasInstrumentationImport)(expressServerPath, originalExpressServerMod.$code)) {
prompts_1.default.log.warn(`File ${chalk_1.default.cyan(path.basename(expressServerPath))} already contains instrumentation import.
Skipping adding instrumentation functionality to ${chalk_1.default.cyan(path.basename(expressServerPath))}.`);
return true;
}
originalExpressServerMod.$code = `import './${instrumentationFile}';\n${originalExpressServerMod.$code}`;
fs.writeFileSync(expressServerPath, originalExpressServerMod.$code);
return true;
}
exports.insertServerInstrumentationFile = insertServerInstrumentationFile;
function isRemixV2(packageJson) {
const remixVersion = (0, package_json_1.getPackageVersion)('@remix-run/react', packageJson);
if (!remixVersion) {
return false;
}
const minVer = (0, semver_1.minVersion)(remixVersion);
if (!minVer) {
return false;
}
return (0, semver_1.gte)(minVer, '2.0.0');
}
exports.isRemixV2 = isRemixV2;
async function loadRemixConfig() {
const configFilePath = path.join(process.cwd(), REMIX_CONFIG_FILE);
try {
if (!fs.existsSync(configFilePath)) {
return {};
}
const configUrl = url.pathToFileURL(configFilePath).href;
const remixConfigModule = (await import(configUrl));
return remixConfigModule?.default || {};
}
catch (e) {
prompts_1.default.log.error(`Couldn't load ${REMIX_CONFIG_FILE}.`);
prompts_1.default.log.info(chalk_1.default.dim(typeof e === 'object' && e != null && 'toString' in e
? e.toString()
: typeof e === 'string'
? e
: 'Unknown error'));
return {};
}
}
exports.loadRemixConfig = loadRemixConfig;
async function instrumentRootRoute(isTS) {
const rootFilename = `root.${isTS ? 'tsx' : 'jsx'}`;
await (0, root_1.instrumentRoot)(rootFilename);
prompts_1.default.log.success(`Successfully instrumented root route ${chalk_1.default.cyan(rootFilename)}.`);
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
exports.instrumentRootRoute = instrumentRootRoute;
async function updateBuildScript(args) {
const packageJson = await (0, clack_1.getPackageDotJson)();
if (!packageJson.scripts) {
packageJson.scripts = {};
}
const buildCommand = args.isHydrogen
? 'shopify hydrogen build'
: 'remix build';
const instrumentedBuildCommand = `${buildCommand} --sourcemap && sentry-upload-sourcemaps --org ${args.org} --project ${args.project}` +
(args.url ? ` --url ${args.url}` : '') +
(args.isHydrogen ? ' --buildPath ./dist' : '');
if (!packageJson.scripts.build) {
packageJson.scripts.build = instrumentedBuildCommand;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
}
else if (packageJson.scripts.build.includes(buildCommand)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
packageJson.scripts.build = packageJson.scripts.build.replace(buildCommand, instrumentedBuildCommand);
}
else {
throw new Error("`build` script doesn't contain a known build command. Please update it manually.");
}
await fs.promises.writeFile(path.join(process.cwd(), 'package.json'), JSON.stringify(packageJson, null, 2));
prompts_1.default.log.success(`Successfully updated ${chalk_1.default.cyan('build')} script in ${chalk_1.default.cyan('package.json')} to generate and upload sourcemaps.`);
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
exports.updateBuildScript = updateBuildScript;
function updateEntryClientMod(
// MagicAst returns `ProxifiedModule<any>` so therefore we have to use `any` here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalEntryClientMod, dsn, selectedFeatures) {
const imports = ['init'];
if (selectedFeatures.replay) {
imports.push('replayIntegration');
}
if (selectedFeatures.performance) {
imports.push('browserTracingIntegration');
}
originalEntryClientMod.imports.$add({
from: '@sentry/remix',
imported: `${imports.join(', ')}`,
});
if (selectedFeatures.performance) {
originalEntryClientMod.imports.$add({
from: '@remix-run/react',
imported: 'useLocation',
local: 'useLocation',
});
originalEntryClientMod.imports.$add({
from: '@remix-run/react',
imported: 'useMatches',
local: 'useMatches',
});
originalEntryClientMod.imports.$add({
from: 'react',
imported: 'useEffect',
local: 'useEffect',
});
}
insertClientInitCall(dsn, originalEntryClientMod, selectedFeatures);
return originalEntryClientMod;
}
exports.updateEntryClientMod = updateEntryClientMod;
async function initializeSentryOnEntryClient(dsn, isTS, selectedFeatures) {
const clientEntryFilename = `entry.client.${isTS ? 'tsx' : 'jsx'}`;
const originalEntryClient = path.join(process.cwd(), 'app', clientEntryFilename);
const originalEntryClientMod = await (0, magicast_1.loadFile)(originalEntryClient);
if ((0, utils_1.hasSentryContent)(originalEntryClient, originalEntryClientMod.$code)) {
return;
}
const updatedEntryClientMod = updateEntryClientMod(originalEntryClientMod, dsn, selectedFeatures);
await (0, magicast_1.writeFile)(updatedEntryClientMod.$ast, path.join(process.cwd(), 'app', clientEntryFilename));
prompts_1.default.log.success(`Successfully initialized Sentry on client entry point ${chalk_1.default.cyan(clientEntryFilename)}`);
}
exports.initializeSentryOnEntryClient = initializeSentryOnEntryClient;
async function updateStartScript(instrumentationFile) {
const packageJson = await (0, clack_1.getPackageDotJson)();
if (!packageJson.scripts || !packageJson.scripts.start) {
throw new Error("Couldn't find a `start` script in your package.json. Please add one manually.");
}
if (packageJson.scripts.start.includes('NODE_OPTIONS')) {
prompts_1.default.log.warn(`Found existing NODE_OPTIONS in ${chalk_1.default.cyan('start')} script. Skipping adding Sentry initialization.`);
return;
}
if (!packageJson.scripts.start.includes('remix-serve') &&
// Adding a following empty space not to match a path that includes `node`
!packageJson.scripts.start.includes('node ')) {
prompts_1.default.log.warn(`Found a ${chalk_1.default.cyan('start')} script that doesn't use ${chalk_1.default.cyan('remix-serve')} or ${chalk_1.default.cyan('node')}. Skipping adding Sentry initialization.`);
return;
}
const startCommand = packageJson.scripts.start;
packageJson.scripts.start = `NODE_OPTIONS='--import ./${instrumentationFile}' ${startCommand}`;
await fs.promises.writeFile(path.join(process.cwd(), 'package.json'), JSON.stringify(packageJson, null, 2));
prompts_1.default.log.success(`Successfully updated ${chalk_1.default.cyan('start')} script in ${chalk_1.default.cyan('package.json')} to include Sentry initialization on start.`);
}
exports.updateStartScript = updateStartScript;
async function instrumentSentryOnEntryServer(isTS) {
const serverEntryFilename = `entry.server.${isTS ? 'tsx' : 'jsx'}`;
const originalEntryServer = path.join(process.cwd(), 'app', serverEntryFilename);
const originalEntryServerMod = await (0, magicast_1.loadFile)(originalEntryServer);
if ((0, utils_1.hasSentryContent)(originalEntryServer, originalEntryServerMod.$code)) {
return;
}
originalEntryServerMod.imports.$add({
from: '@sentry/remix',
imported: '*',
local: 'Sentry',
});
const handleErrorInstrumented = (0, handle_error_1.instrumentHandleError)(originalEntryServerMod, serverEntryFilename);
if (handleErrorInstrumented) {
prompts_1.default.log.success(`Instrumented ${chalk_1.default.cyan('handleError')} in ${chalk_1.default.cyan(`${serverEntryFilename}`)}`);
}
await (0, magicast_1.writeFile)(originalEntryServerMod.$ast, path.join(process.cwd(), 'app', serverEntryFilename));
prompts_1.default.log.success(`Successfully initialized Sentry on server entry point ${chalk_1.default.cyan(serverEntryFilename)}.`);
}
exports.instrumentSentryOnEntryServer = instrumentSentryOnEntryServer;
//# sourceMappingURL=sdk-setup.js.map