@sentry/wizard
Version:
Sentry wizard helping you to configure your project
448 lines (447 loc) • 21.2 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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadSvelteConfig = exports.createOrMergeSvelteKitFiles = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const url = __importStar(require("url"));
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");
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
const helpers_1 = require("magicast/helpers");
const templates_1 = require("./templates");
const clack_1 = require("../utils/clack");
const debug_1 = require("../utils/debug");
const ast_utils_1 = require("../utils/ast-utils");
const telemetry_1 = require("../telemetry");
const SVELTE_CONFIG_FILE = 'svelte.config.js';
async function createOrMergeSvelteKitFiles(projectInfo, svelteConfig) {
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 viteConfig = (0, ast_utils_1.findFile)(path.resolve(process.cwd(), 'vite.config'));
const fileEnding = (0, clack_1.isUsingTypeScript)() ? 'ts' : 'js';
const { dsn } = projectInfo;
Sentry.setTag('client-hooks-file-strategy', originalClientHooksFile ? 'merge' : 'create');
if (!originalClientHooksFile) {
prompts_1.default.log.info('No client hooks file found, creating a new one.');
await createNewHooksFile(`${clientHooksPath}.${fileEnding}`, 'client', dsn, selectedFeatures);
}
else {
await mergeHooksFile(originalClientHooksFile, 'client', dsn, selectedFeatures);
}
Sentry.setTag('server-hooks-file-strategy', originalServerHooksFile ? 'merge' : 'create');
if (!originalServerHooksFile) {
prompts_1.default.log.info('No server hooks file found, creating a new one.');
await createNewHooksFile(`${serverHooksPath}.${fileEnding}`, 'server', dsn, selectedFeatures);
}
else {
await mergeHooksFile(originalServerHooksFile, 'server', dsn, selectedFeatures);
}
if (viteConfig) {
await 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) {
const filledTemplate = hooktype === 'client'
? (0, templates_1.getClientHooksTemplate)(dsn, selectedFeatures)
: (0, templates_1.getServerHooksTemplate)(dsn, selectedFeatures);
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');
}
/**
* 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) {
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 modifyAndRecordFail(() => originalHooksMod.imports.$add({
from: '@sentry/sveltekit',
imported: '*',
local: 'Sentry',
}), 'import-injection', file);
await modifyAndRecordFail(() => {
if (hookType === 'client') {
insertClientInitCall(dsn, originalHooksMod, selectedFeatures);
}
else {
insertServerInitCall(dsn, originalHooksMod, selectedFeatures);
}
}, 'init-call-injection', file);
await modifyAndRecordFail(() => wrapHandleError(originalHooksMod), 'wrap-handle-error', file);
if (hookType === 'server') {
await modifyAndRecordFail(() => wrapHandle(originalHooksMod), 'wrap-handle', 'server-hooks');
}
await 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');
}
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;
}
// 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);
}
function insertServerInitCall(dsn,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalHooksMod, selectedFeatures) {
const initArgs = {
dsn,
};
if (selectedFeatures.performance) {
initArgs.tracesSampleRate = 1.0;
}
if (selectedFeatures.logs) {
initArgs.enableLogs = 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 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)(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.
}
}
async function loadSvelteConfig() {
const configFilePath = path.join(process.cwd(), SVELTE_CONFIG_FILE);
try {
if (!fs.existsSync(configFilePath)) {
return {};
}
const configUrl = url.pathToFileURL(configFilePath).href;
const svelteConfigModule = (await import(configUrl));
return svelteConfigModule?.default || {};
}
catch (e) {
prompts_1.default.log.error(`Couldn't load ${SVELTE_CONFIG_FILE}.
Please make sure, you're running this wizard with Node 16 or newer`);
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.loadSvelteConfig = loadSvelteConfig;
async function modifyViteConfig(viteConfigPath, projectInfo) {
const viteConfigContent = (await fs.promises.readFile(viteConfigPath, 'utf-8')).toString();
const { org, project, url, selfHosted } = projectInfo;
const prettyViteConfigFilename = chalk_1.default.cyan(path.basename(viteConfigPath));
try {
const viteModule = (0, magicast_1.parseModule)(viteConfigContent);
if ((0, ast_utils_1.hasSentryContent)(viteModule.$ast)) {
prompts_1.default.log.warn(`File ${prettyViteConfigFilename} already contains Sentry code.
Skipping adding Sentry functionality to.`);
Sentry.setTag(`modified-vite-cfg`, 'fail');
Sentry.setTag(`vite-cfg-fail-reason`, 'has-sentry-content');
return;
}
await modifyAndRecordFail(() => (0, helpers_1.addVitePlugin)(viteModule, {
imported: 'sentrySvelteKit',
from: '@sentry/sveltekit',
constructor: 'sentrySvelteKit',
options: {
sourceMapsUploadOptions: {
org,
project,
...(selfHosted && { url }),
},
},
index: 0,
}), 'add-vite-plugin', 'vite-cfg');
await modifyAndRecordFail(async () => {
const code = (0, magicast_1.generateCode)(viteModule.$ast).code;
await fs.promises.writeFile(viteConfigPath, code);
}, 'write-file', 'vite-cfg');
}
catch (e) {
(0, debug_1.debug)(e);
await showFallbackViteCopyPasteSnippet(viteConfigPath, getViteConfigCodeSnippet(org, project, selfHosted, url));
Sentry.captureException('Sveltekit Vite Config Modification Fail');
}
prompts_1.default.log.success(`Added Sentry code to ${prettyViteConfigFilename}`);
Sentry.setTag(`modified-vite-cfg`, 'success');
}
async function showFallbackViteCopyPasteSnippet(viteConfigPath, codeSnippet) {
const viteConfigFilename = path.basename(viteConfigPath);
prompts_1.default.log.warning(`Couldn't automatically modify your ${chalk_1.default.cyan(viteConfigFilename)}
${chalk_1.default.dim(`This sometimes happens when we encounter more complex vite configs.
It may not seem like it but sometimes our magical powers are limited ;)`)}`);
prompts_1.default.log.info("But don't worry - it's super easy to do this yourself!");
prompts_1.default.log.step(`Add the following code to your ${chalk_1.default.cyan(viteConfigFilename)}:`);
// Intentionally logging to console here for easier copy/pasting
// eslint-disable-next-line no-console
console.log(codeSnippet);
await (0, clack_1.abortIfCancelled)(prompts_1.default.select({
message: 'Did you copy the snippet above?',
options: [
{ label: 'Yes!', value: true, hint: "Great, that's already it!" },
],
initialValue: true,
}));
}
const getViteConfigCodeSnippet = (org, project, selfHosted, url) => chalk_1.default.gray(`
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
${chalk_1.default.greenBright("import { sentrySvelteKit } from '@sentry/sveltekit'")}
export default defineConfig({
plugins: [
// Make sure \`sentrySvelteKit\` is registered before \`sveltekit\`
${chalk_1.default.greenBright(`sentrySvelteKit({
sourceMapsUploadOptions: {
org: '${org}',
project: '${project}',${selfHosted ? `\n url: '${url}',` : ''}
}
}),`)}
sveltekit(),
]
});
`);
/**
* We want to insert the init call on top of the file but after all import statements
*/
function getInitCallInsertionIndex(originalHooksModAST) {
// We need to deep-copy here because reverse mutates in place
const copiedBodyNodes = [...originalHooksModAST.body];
const lastImportDeclaration = copiedBodyNodes
.reverse()
.find((node) => node.type === 'ImportDeclaration');
const initCallInsertionIndex = lastImportDeclaration
? originalHooksModAST.body.indexOf(lastImportDeclaration) + 1
: 0;
return initCallInsertionIndex;
}
/**
* Applies the @param modifyCallback and records Sentry tags if the call failed.
* In case of a failure, a tag is set with @param reason as a fail reason
* and the error is rethrown.
*/
async function modifyAndRecordFail(modifyCallback, reason, fileType) {
try {
await (0, telemetry_1.traceStep)(`${fileType}-${reason}`, modifyCallback);
}
catch (e) {
Sentry.setTag(`modified-${fileType}`, 'fail');
Sentry.setTag(`${fileType}-mod-fail-reason`, reason);
throw e;
}
}
//# sourceMappingURL=sdk-setup.js.map