UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

305 lines 15.9 kB
"use strict"; /* 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