@sentry/wizard
Version:
Sentry wizard helping you to configure your project
275 lines • 14.6 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* 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.instrumentHandleError = exports.instrumentHandleRequest = exports.instrumentServerEntry = void 0;
const recast = __importStar(require("recast"));
// @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"));
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
const magicast_1 = require("magicast");
const debug_1 = require("../../utils/debug");
const ast_utils_1 = require("../../utils/ast-utils");
const utils_1 = require("./utils");
async function instrumentServerEntry(serverEntryPath) {
const serverEntryAst = await (0, magicast_1.loadFile)(serverEntryPath);
if (!(0, ast_utils_1.hasSentryContent)(serverEntryAst.$ast)) {
serverEntryAst.imports.$add({
from: '@sentry/react-router',
imported: '*',
local: 'Sentry',
});
}
instrumentHandleError(serverEntryAst);
instrumentHandleRequest(serverEntryAst);
await (0, magicast_1.writeFile)(serverEntryAst.$ast, serverEntryPath);
}
exports.instrumentServerEntry = instrumentServerEntry;
function instrumentHandleRequest(originalEntryServerMod) {
const originalEntryServerModAST = originalEntryServerMod.$ast;
const defaultServerEntryExport = originalEntryServerModAST.body.find((node) => {
return node.type === 'ExportDefaultDeclaration';
});
if (!defaultServerEntryExport) {
prompts_1.default.log.warn(`Could not find function ${chalk_1.default.cyan('handleRequest')} in your server entry file. Creating one for you.`);
let foundServerRouterImport = false;
let foundRenderToPipeableStreamImport = false;
let foundCreateReadableStreamFromReadableImport = false;
originalEntryServerMod.imports.$items.forEach((item) => {
if (item.imported === 'ServerRouter' && item.from === 'react-router') {
foundServerRouterImport = true;
}
if (item.imported === 'renderToPipeableStream' &&
item.from === 'react-dom/server') {
foundRenderToPipeableStreamImport = true;
}
if (item.imported === 'createReadableStreamFromReadable' &&
item.from === '@react-router/node') {
foundCreateReadableStreamFromReadableImport = true;
}
});
if (!foundServerRouterImport) {
originalEntryServerMod.imports.$add({
from: 'react-router',
imported: 'ServerRouter',
local: 'ServerRouter',
});
}
if (!foundRenderToPipeableStreamImport) {
originalEntryServerMod.imports.$add({
from: 'react-dom/server',
imported: 'renderToPipeableStream',
local: 'renderToPipeableStream',
});
}
if (!foundCreateReadableStreamFromReadableImport) {
originalEntryServerMod.imports.$add({
from: '@react-router/node',
imported: 'createReadableStreamFromReadable',
local: 'createReadableStreamFromReadable',
});
}
const implementation = recast.parse(`const handleRequest = Sentry.createSentryHandleRequest({
ServerRouter,
renderToPipeableStream,
createReadableStreamFromReadable,
})`).program.body[0];
try {
originalEntryServerModAST.body.splice((0, utils_1.getAfterImportsInsertionIndex)(originalEntryServerModAST), 0, implementation);
originalEntryServerModAST.body.push({
type: 'ExportDefaultDeclaration',
declaration: {
type: 'Identifier',
name: 'handleRequest',
},
});
}
catch (error) {
(0, debug_1.debug)('Failed to insert handleRequest implementation:', error);
throw new Error('Could not automatically instrument handleRequest. Please add it manually.');
}
}
else if (defaultServerEntryExport &&
// @ts-expect-error - StatementKind works here because the AST is proxified by magicast
(0, magicast_1.generateCode)(defaultServerEntryExport).code.includes('wrapSentryHandleRequest')) {
(0, debug_1.debug)('wrapSentryHandleRequest is already used, skipping wrapping again');
prompts_1.default.log.info('Sentry handleRequest wrapper already detected, skipping instrumentation.');
}
else {
let defaultExportNode = null;
const defaultExportIndex = originalEntryServerModAST.body.findIndex((node) => {
const found = node.type === 'ExportDefaultDeclaration';
if (found) {
defaultExportNode = node;
}
return found;
});
if (defaultExportIndex !== -1 && defaultExportNode !== null) {
recast.visit(defaultExportNode, {
visitCallExpression(path) {
if ((0, ast_utils_1.safeCalleeIdentifierMatch)(path.value.callee, 'pipe') &&
path.value.arguments.length &&
path.value.arguments[0].type === 'Identifier' &&
(0, ast_utils_1.safeGetIdentifierName)(path.value.arguments[0]) === 'body') {
const wrapped = recast.types.builders.callExpression(recast.types.builders.memberExpression(recast.types.builders.identifier('Sentry'), recast.types.builders.identifier('getMetaTagTransformer')), [path.value.arguments[0]]);
path.value.arguments[0] = wrapped;
}
this.traverse(path);
},
});
// Replace the existing default export with the wrapped one
originalEntryServerModAST.body.splice(defaultExportIndex, 1,
// @ts-expect-error - declaration works here because the AST is proxified by magicast
defaultExportNode.declaration);
// Adding our wrapped export
originalEntryServerModAST.body.push(recast.types.builders.exportDefaultDeclaration(recast.types.builders.callExpression(recast.types.builders.memberExpression(recast.types.builders.identifier('Sentry'), recast.types.builders.identifier('wrapSentryHandleRequest')), [recast.types.builders.identifier('handleRequest')])));
}
}
}
exports.instrumentHandleRequest = instrumentHandleRequest;
function instrumentHandleError(originalEntryServerMod) {
const originalEntryServerModAST = originalEntryServerMod.$ast;
const handleErrorFunctionExport = originalEntryServerModAST.body.find((node) => {
return (node.type === 'ExportNamedDeclaration' &&
node.declaration?.type === 'FunctionDeclaration' &&
node.declaration.id?.name === 'handleError');
});
const handleErrorFunctionVariableDeclarationExport = originalEntryServerModAST.body.find((node) => {
if (node.type !== 'ExportNamedDeclaration' ||
node.declaration?.type !== 'VariableDeclaration') {
return false;
}
const declarations = node.declaration.declarations;
if (!declarations || declarations.length === 0) {
return false;
}
const firstDeclaration = declarations[0];
if (!firstDeclaration || firstDeclaration.type !== 'VariableDeclarator') {
return false;
}
const id = firstDeclaration.id;
return id && id.type === 'Identifier' && id.name === 'handleError';
});
if (!handleErrorFunctionExport &&
!handleErrorFunctionVariableDeclarationExport) {
prompts_1.default.log.warn(`Could not find function ${chalk_1.default.cyan('handleError')} in your server entry file. Creating one for you.`);
const implementation = recast.parse(`const handleError = Sentry.createSentryHandleError({
logErrors: false
})`).program.body[0];
originalEntryServerModAST.body.splice((0, utils_1.getAfterImportsInsertionIndex)(originalEntryServerModAST), 0, recast.types.builders.exportNamedDeclaration(implementation));
}
else if ((handleErrorFunctionExport &&
// @ts-expect-error - StatementKind works here because the AST is proxified by magicast
(0, magicast_1.generateCode)(handleErrorFunctionExport).code.includes('captureException')) ||
(handleErrorFunctionVariableDeclarationExport &&
// @ts-expect-error - StatementKind works here because the AST is proxified by magicast
(0, magicast_1.generateCode)(handleErrorFunctionVariableDeclarationExport).code.includes('captureException'))) {
(0, debug_1.debug)('Found captureException inside handleError, skipping adding it again');
}
else if ((handleErrorFunctionExport &&
// @ts-expect-error - StatementKind works here because the AST is proxified by magicast
(0, magicast_1.generateCode)(handleErrorFunctionExport).code.includes('createSentryHandleError')) ||
(handleErrorFunctionVariableDeclarationExport &&
// @ts-expect-error - StatementKind works here because the AST is proxified by magicast
(0, magicast_1.generateCode)(handleErrorFunctionVariableDeclarationExport).code.includes('createSentryHandleError'))) {
(0, debug_1.debug)('createSentryHandleError is already used, skipping adding it again');
}
else if (handleErrorFunctionExport) {
// Create the Sentry captureException call as an IfStatement
const sentryCall = recast.parse(`if (!request.signal.aborted) {
Sentry.captureException(error);
}`).program.body[0];
// Safely insert the Sentry call at the beginning of the handleError function body
// @ts-expect-error - declaration works here because the AST is proxified by magicast
const declaration = handleErrorFunctionExport.declaration;
if (declaration &&
declaration.body &&
declaration.body.body &&
Array.isArray(declaration.body.body)) {
declaration.body.body.unshift(sentryCall);
}
else {
(0, debug_1.debug)('Cannot safely access handleError function body, skipping instrumentation');
}
}
else if (handleErrorFunctionVariableDeclarationExport) {
// Create the Sentry captureException call as an IfStatement
const sentryCall = recast.parse(`if (!request.signal.aborted) {
Sentry.captureException(error);
}`).program.body[0];
// Safe access to existing handle error implementation with proper null checks
// We know this is ExportNamedDeclaration with VariableDeclaration from the earlier find
const exportDeclaration = handleErrorFunctionVariableDeclarationExport;
if (!exportDeclaration.declaration ||
exportDeclaration.declaration.type !== 'VariableDeclaration' ||
!exportDeclaration.declaration.declarations ||
exportDeclaration.declaration.declarations.length === 0) {
(0, debug_1.debug)('Cannot safely access handleError variable declaration, skipping instrumentation');
return;
}
const firstDeclaration = exportDeclaration.declaration.declarations[0];
if (!firstDeclaration ||
firstDeclaration.type !== 'VariableDeclarator' ||
!firstDeclaration.init) {
(0, debug_1.debug)('Cannot safely access handleError variable declarator init, skipping instrumentation');
return;
}
const existingHandleErrorImplementation = firstDeclaration.init;
const existingParams = existingHandleErrorImplementation.params;
const existingBody = existingHandleErrorImplementation.body;
const requestParam = {
...recast.types.builders.property('init', recast.types.builders.identifier('request'), // key
recast.types.builders.identifier('request')),
shorthand: true,
};
// Add error and {request} parameters to handleError function if not present
// When none of the parameters exist
if (existingParams.length === 0) {
existingParams.push(recast.types.builders.identifier('error'), recast.types.builders.objectPattern([requestParam]));
// When only error parameter exists
}
else if (existingParams.length === 1) {
existingParams.push(recast.types.builders.objectPattern([requestParam]));
// When both parameters exist, but request is not destructured
}
else if (existingParams[1].type === 'ObjectPattern' &&
!existingParams[1].properties.some((prop) => (0, ast_utils_1.safeGetIdentifierName)(prop.key) === 'request')) {
existingParams[1].properties.push(requestParam);
}
// Add the Sentry call to the function body
existingBody.body.push(sentryCall);
}
}
exports.instrumentHandleError = instrumentHandleError;
//# sourceMappingURL=server-entry.js.map