UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

407 lines 20.3 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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.insertClientInitCall = exports.createOrMergeSvelteKitFiles = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const chalk_1 = __importDefault(require("chalk")); const Sentry = __importStar(require("@sentry/node")); //@ts-expect-error - clack is ESM and TS complains about that. It works though const prompts_1 = __importDefault(require("@clack/prompts")); // @ts-expect-error - magicast is ESM and TS complains about that. It works though const magicast_1 = require("magicast"); const templates_1 = require("../templates"); const clack_1 = require("../../utils/clack"); const ast_utils_1 = require("../../utils/ast-utils"); const svelte_config_1 = require("./svelte-config"); const vite_1 = require("./vite"); const utils_1 = require("./utils"); const debug_1 = require("../../utils/debug"); async function createOrMergeSvelteKitFiles(projectInfo, svelteConfig, setupForSvelteKitTracing) { const selectedFeatures = await (0, clack_1.featureSelectionPrompt)([ { id: 'performance', prompt: `Do you want to enable ${chalk_1.default.bold('Tracing')} to track the performance of your application?`, enabledHint: 'recommended', }, { id: 'replay', prompt: `Do you want to enable ${chalk_1.default.bold('Session Replay')} to get a video-like reproduction of errors during a user session?`, enabledHint: 'recommended, but increases bundle size', }, { id: 'logs', prompt: `Do you want to enable ${chalk_1.default.bold('Logs')} to send your application logs to Sentry?`, enabledHint: 'recommended', }, ]); const { clientHooksPath, serverHooksPath } = getHooksConfigDirs(svelteConfig); // full file paths with correct file ending (or undefined if not found) const originalClientHooksFile = (0, ast_utils_1.findFile)(clientHooksPath); const originalServerHooksFile = (0, ast_utils_1.findFile)(serverHooksPath); const originalInstrumentationServerFile = (0, ast_utils_1.findFile)(path.resolve(process.cwd(), 'src', 'instrumentation.server')); const viteConfig = (0, ast_utils_1.findFile)(path.resolve(process.cwd(), 'vite.config')); const fileEnding = (0, clack_1.isUsingTypeScript)() ? 'ts' : 'js'; const { dsn } = projectInfo; if (setupForSvelteKitTracing) { await (0, svelte_config_1.enableTracingAndInstrumentation)(svelteConfig, selectedFeatures.performance); try { if (!originalInstrumentationServerFile) { await createNewInstrumentationServerFile(dsn, selectedFeatures); } else { await mergeInstrumentationServerFile(originalInstrumentationServerFile, dsn, selectedFeatures); } } catch (e) { prompts_1.default.log.warn(`Failed to automatically set up ${chalk_1.default.cyan(`instrumentation.server.${fileEnding ?? (0, clack_1.isUsingTypeScript)() ? 'ts' : 'js'}`)}.`); (0, debug_1.debug)(e); await (0, clack_1.showCopyPasteInstructions)({ codeSnippet: (0, templates_1.getInstrumentationServerTemplate)(dsn, selectedFeatures), filename: `instrumentation.server.${fileEnding ?? (0, clack_1.isUsingTypeScript)() ? 'ts' : 'js'}`, }); Sentry.setTag('created-instrumentation-server', 'fail'); } } Sentry.setTag('server-hooks-file-strategy', originalServerHooksFile ? 'merge' : 'create'); if (!originalServerHooksFile) { await createNewHooksFile(`${serverHooksPath}.${fileEnding}`, 'server', dsn, selectedFeatures, !setupForSvelteKitTracing); } else { await mergeHooksFile(originalServerHooksFile, 'server', dsn, selectedFeatures, !setupForSvelteKitTracing); } Sentry.setTag('client-hooks-file-strategy', originalClientHooksFile ? 'merge' : 'create'); if (!originalClientHooksFile) { await createNewHooksFile(`${clientHooksPath}.${fileEnding}`, 'client', dsn, selectedFeatures, true); } else { await mergeHooksFile(originalClientHooksFile, 'client', dsn, selectedFeatures, true); } if (viteConfig) { await (0, vite_1.modifyViteConfig)(viteConfig, projectInfo); } } exports.createOrMergeSvelteKitFiles = createOrMergeSvelteKitFiles; /** * Attempts to read the svelte.config.js file to find the location of the hooks files. * If users specified a custom location, we'll use that. Otherwise, we'll use the default. */ function getHooksConfigDirs(svelteConfig) { const relativeUserClientHooksPath = svelteConfig?.kit?.files?.hooks?.client; const relativeUserServerHooksPath = svelteConfig?.kit?.files?.hooks?.server; const userClientHooksPath = relativeUserClientHooksPath && path.resolve(process.cwd(), relativeUserClientHooksPath); const userServerHooksPath = relativeUserServerHooksPath && path.resolve(process.cwd(), relativeUserServerHooksPath); const defaulHooksDir = path.resolve(process.cwd(), 'src'); const defaultClientHooksPath = path.resolve(defaulHooksDir, 'hooks.client'); // file ending missing on purpose const defaultServerHooksPath = path.resolve(defaulHooksDir, 'hooks.server'); // same here return { clientHooksPath: userClientHooksPath || defaultClientHooksPath, serverHooksPath: userServerHooksPath || defaultServerHooksPath, }; } /** * Reads the template, replaces the dsn placeholder with the actual dsn and writes the file to @param hooksFileDest */ async function createNewHooksFile(hooksFileDest, hooktype, dsn, selectedFeatures, setupForSvelteKitTracing) { const filledTemplate = hooktype === 'client' ? (0, templates_1.getClientHooksTemplate)(dsn, selectedFeatures) : (0, templates_1.getServerHooksTemplate)(dsn, selectedFeatures, setupForSvelteKitTracing); await fs.promises.mkdir(path.dirname(hooksFileDest), { recursive: true }); await fs.promises.writeFile(hooksFileDest, filledTemplate); prompts_1.default.log.success(`Created ${hooksFileDest}`); Sentry.setTag(`created-${hooktype}-hooks`, 'success'); } async function createNewInstrumentationServerFile(dsn, selectedFeatures) { const filledTemplate = (0, templates_1.getInstrumentationServerTemplate)(dsn, selectedFeatures); const fileEnding = (0, clack_1.isUsingTypeScript)() ? 'ts' : 'js'; const instrumentationServerFile = path.resolve(process.cwd(), 'src', `instrumentation.server.${fileEnding}`); await fs.promises.mkdir(path.dirname(instrumentationServerFile), { recursive: true, }); await fs.promises.writeFile(instrumentationServerFile, filledTemplate); prompts_1.default.log.success(`Created ${chalk_1.default.cyan(path.basename(instrumentationServerFile))}`); Sentry.setTag('created-instrumentation-server', 'success'); } /** * Merges the users' hooks file with Sentry-related code. * * Both hooks: * - add import * as Sentry * - add Sentry.init * - add handleError hook wrapper * * Additionally in Server hook: * - add handle hook handler */ async function mergeHooksFile(hooksFile, hookType, dsn, selectedFeatures, includeSentryInit) { const originalHooksMod = await (0, magicast_1.loadFile)(hooksFile); const file = `${hookType}-hooks`; if ((0, ast_utils_1.hasSentryContent)(originalHooksMod.$ast)) { // We don't want to mess with files that already have Sentry content. // Let's just bail out at this point. prompts_1.default.log.warn(`File ${chalk_1.default.cyan(path.basename(hooksFile))} already contains Sentry code. Skipping adding Sentry functionality to.`); Sentry.setTag(`modified-${file}`, 'fail'); Sentry.setTag(`${file}-fail-reason`, 'has-sentry-content'); return; } await (0, utils_1.modifyAndRecordFail)(() => originalHooksMod.imports.$add({ from: '@sentry/sveltekit', imported: '*', local: 'Sentry', }), 'import-injection', file); if (hookType === 'client' || includeSentryInit) { await (0, utils_1.modifyAndRecordFail)(() => { if (hookType === 'client') { insertClientInitCall(dsn, originalHooksMod, selectedFeatures); } else { insertServerInitCall(dsn, originalHooksMod, selectedFeatures); } }, 'init-call-injection', file); } await (0, utils_1.modifyAndRecordFail)(() => wrapHandleError(originalHooksMod), 'wrap-handle-error', file); if (hookType === 'server') { await (0, utils_1.modifyAndRecordFail)(() => wrapHandle(originalHooksMod), 'wrap-handle', 'server-hooks'); } await (0, utils_1.modifyAndRecordFail)(async () => { const modifiedCode = originalHooksMod.generate().code; await fs.promises.writeFile(hooksFile, modifiedCode); }, 'write-file', file); prompts_1.default.log.success(`Added Sentry code to ${hooksFile}`); Sentry.setTag(`modified-${hookType}-hooks`, 'success'); } /** * Merges the users' instrumentation.server file with Sentry-related code. * * Both hooks: * - add import * as Sentry * - add Sentry.init * - add handleError hook wrapper * * Additionally in Server hook: * - add handle hook handler */ async function mergeInstrumentationServerFile(instrumentationServerFilePath, dsn, selectedFeatures) { const originalInstrumentationServerMod = await (0, magicast_1.loadFile)(instrumentationServerFilePath); const filename = path.basename(instrumentationServerFilePath); if ((0, ast_utils_1.hasSentryContent)(originalInstrumentationServerMod.$ast)) { // We don't want to mess with files that already have Sentry content. // Let's just bail out at this point. prompts_1.default.log.warn(`File ${chalk_1.default.cyan(filename)} already contains Sentry code. Skipping adding Sentry functionality to it.`); Sentry.setTag(`modified-instrumentation-server`, 'fail'); Sentry.setTag(`instrumentation-server-fail-reason`, 'has-sentry-content'); return; } await (0, utils_1.modifyAndRecordFail)(() => originalInstrumentationServerMod.imports.$add({ from: '@sentry/sveltekit', imported: '*', local: 'Sentry', }), 'import-injection', 'instrumentation-server'); await (0, utils_1.modifyAndRecordFail)(() => { insertServerInitCall(dsn, originalInstrumentationServerMod, selectedFeatures); }, 'init-call-injection', 'instrumentation-server'); await (0, utils_1.modifyAndRecordFail)(async () => { const modifiedCode = originalInstrumentationServerMod.generate().code; await fs.promises.writeFile(instrumentationServerFilePath, modifiedCode); }, 'write-file', 'instrumentation-server'); prompts_1.default.log.success(`Added Sentry.init code to ${chalk_1.default.cyan(filename)}`); Sentry.setTag(`modified-instrumentation-server`, 'success'); } function insertClientInitCall(dsn, // eslint-disable-next-line @typescript-eslint/no-explicit-any originalHooksMod, selectedFeatures) { const initCallComment = ` // If you don't want to use Session Replay, remove the \`Replay\` integration, // \`replaysSessionSampleRate\` and \`replaysOnErrorSampleRate\` options.`; const initArgs = { dsn, }; if (selectedFeatures.performance) { initArgs.tracesSampleRate = 1.0; } if (selectedFeatures.replay) { initArgs.replaysSessionSampleRate = 0.1; initArgs.replaysOnErrorSampleRate = 1.0; initArgs.integrations = [magicast_1.builders.functionCall('Sentry.replayIntegration')]; } if (selectedFeatures.logs) { initArgs.enableLogs = true; } initArgs.sendDefaultPii = true; // This assignment of any values is fine because we're just creating a function call in magicast // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const initCall = magicast_1.builders.functionCall('Sentry.init', initArgs); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const initCallWithComment = magicast_1.builders.raw( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument `${initCallComment}\n${(0, magicast_1.generateCode)(initCall).code}`); const originalHooksModAST = originalHooksMod.$ast; const initCallInsertionIndex = getInitCallInsertionIndex(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)(initCallWithComment).code); } exports.insertClientInitCall = insertClientInitCall; function insertServerInitCall(dsn, // eslint-disable-next-line @typescript-eslint/no-explicit-any originalMod, selectedFeatures) { const initArgs = { dsn, }; if (selectedFeatures.performance) { initArgs.tracesSampleRate = 1.0; } if (selectedFeatures.logs) { initArgs.enableLogs = true; } initArgs.sendDefaultPii = true; // This assignment of any values is fine because we're just creating a function call in magicast // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const initCall = magicast_1.builders.functionCall('Sentry.init', initArgs); const originalModAST = originalMod.$ast; const initCallInsertionIndex = getInitCallInsertionIndex(originalModAST); originalModAST.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); } // eslint-disable-next-line @typescript-eslint/no-explicit-any function wrapHandleError(mod) { const modAst = mod.exports.$ast; const namedExports = modAst.body.filter((node) => node.type === 'ExportNamedDeclaration'); let foundHandleError = false; namedExports.forEach((modExport) => { const declaration = modExport.declaration; if (!declaration) { return; } if (declaration.type === 'FunctionDeclaration') { if (!declaration.id || declaration.id.name !== 'handleError') { return; } foundHandleError = true; const userCode = (0, magicast_1.generateCode)(declaration).code; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.handleError = magicast_1.builders.raw(`Sentry.handleErrorWithSentry(${userCode.replace('handleError', '_handleError')})`); // because magicast doesn't overwrite the original function export, we need to remove it manually modAst.body = modAst.body.filter((node) => node !== modExport); } else if (declaration.type === 'VariableDeclaration') { const declarations = declaration.declarations; declarations.forEach((declaration) => { // @ts-expect-error - id should always have a name in this case if (!declaration.id || declaration.id.name !== 'handleError') { return; } foundHandleError = true; const userCode = declaration.init; const stringifiedUserCode = userCode ? (0, magicast_1.generateCode)(userCode).code : ''; // @ts-expect-error - we can just place a string here, magicast will convert it to a node declaration.init = `Sentry.handleErrorWithSentry(${stringifiedUserCode})`; }); } }); if (!foundHandleError) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.handleError = magicast_1.builders.functionCall('Sentry.handleErrorWithSentry'); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any function wrapHandle(mod) { const modAst = mod.exports.$ast; const namedExports = modAst.body.filter((node) => node.type === 'ExportNamedDeclaration'); let foundHandle = false; namedExports.forEach((modExport) => { const declaration = modExport.declaration; if (!declaration) { return; } if (declaration.type === 'FunctionDeclaration') { if (!declaration.id || declaration.id.name !== 'handle') { return; } foundHandle = true; const userCode = (0, magicast_1.generateCode)(declaration).code; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.handle = magicast_1.builders.raw(`sequence(Sentry.sentryHandle(), ${userCode.replace('handle', '_handle')})`); // because of an issue with magicast, we need to remove the original export modAst.body = modAst.body.filter((node) => node !== modExport); } else if (declaration.type === 'VariableDeclaration') { const declarations = declaration.declarations; declarations.forEach((declaration) => { if (!declaration.id || declaration.id.type !== 'Identifier' || (declaration.id.name && declaration.id.name !== 'handle')) { return; } const userCode = declaration.init; const stringifiedUserCode = userCode ? (0, magicast_1.generateCode)(userCode).code : ''; // @ts-expect-error - we can just place a string here, magicast will convert it to a node declaration.init = `sequence(Sentry.sentryHandle(), ${stringifiedUserCode})`; foundHandle = true; }); } }); if (!foundHandle) { // can't use builders.functionCall here because it doesn't yet // support member expressions (Sentry.sentryHandle()) in args // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment mod.exports.handle = magicast_1.builders.raw('sequence(Sentry.sentryHandle())'); } try { mod.imports.$add({ from: '@sveltejs/kit/hooks', imported: 'sequence', local: 'sequence', }); } catch (_) { // It's possible sequence is already imported. in this case, magicast throws but that's fine. } } /** * We want to insert the init call on top of the file but after all import statements */ function getInitCallInsertionIndex(originalModAST) { // We need to deep-copy here because reverse mutates in place const copiedBodyNodes = [...originalModAST.body]; const lastImportDeclaration = copiedBodyNodes .reverse() .find((node) => node.type === 'ImportDeclaration'); const initCallInsertionIndex = lastImportDeclaration ? originalModAST.body.indexOf(lastImportDeclaration) + 1 : 0; return initCallInsertionIndex; } //# sourceMappingURL=setup.js.map