Skip to content

Commit

Permalink
feat(sourcemaps): Automatically insert Sentry Vite plugin in Vite con…
Browse files Browse the repository at this point in the history
…fig (#382)

This PR adds automatic insertion of the Sentry Vite plugin into users' vite config files:
* Check if `vite.config.(js|ts|cjs|mts)` can be found
* If found, add plugin and import (`magicast`)
* If not found or error during insertion, fall back to copy/paste instructions
* Collect telemetry around sucess/failure of modification and failure reasons
  • Loading branch information
Lms24 authored Aug 1, 2023
1 parent f80efc5 commit 6aac8b4
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 47 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

- ref: Add debug logging to clack-based wizards (#381)
- fix: Pin minimum version to Node 14.18 (#383)
- feat(sourcemaps): Automatically insert Sentry Vite plugin in Vite config (#382)
- feat(reactnative): Use `with-environment.sh` in Xcode Build Phases (#329)
- fix(sveltekit): Bump `magicast` to handle vite configs declared as variables (#380)
- ref(sveltekit): Add vite plugin insertion fallback mechanism (#379)
- ref(sveltekit): Insert project config into vite config instead of `sentry.properties` (#378)
- feat(reactnative): Use `with-environment.sh` in Xcode Build Phases (#329)

## 3.8.0

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@
"build": "yarn tsc",
"postbuild": "chmod +x ./dist/bin.js && cp -r scripts/** dist",
"lint": "yarn lint:prettier && yarn lint:eslint",
"lint:prettier": "prettier --check \"{lib,src}/**/*.ts\"",
"lint:prettier": "prettier --check \"{lib,src,test}/**/*.ts\"",
"lint:eslint": "eslint . --cache --format stylish",
"fix": "yarn fix:eslint && yarn fix:prettier",
"fix:prettier": "prettier --write \"{lib,src}/**/*.ts\"",
"fix:prettier": "prettier --write \"{lib,src,test}/**/*.ts\"",
"fix:eslint": "eslint . --format stylish --fix",
"test": "yarn build && jest",
"try": "ts-node bin.ts",
"try:uninstall": "ts-node bin.ts --uninstall",
"test:watch": "jest --watch --notify"
"test:watch": "jest --watch"
},
"jest": {
"collectCoverage": true,
Expand Down
113 changes: 101 additions & 12 deletions src/sourcemaps/tools/vite.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// @ts-ignore - clack is ESM and TS complains about that. It works though
import clack, { select } from '@clack/prompts';
// @ts-ignore - magicast is ESM and TS complains about that. It works though
import { generateCode, parseModule } from 'magicast';
// @ts-ignore - magicast is ESM and TS complains about that. It works though
import { addVitePlugin } from 'magicast/helpers';

import * as Sentry from '@sentry/node';

import chalk from 'chalk';
import {
abortIfCancelled,
Expand All @@ -13,6 +20,11 @@ import {
SourceMapUploadToolConfigurationFunction,
SourceMapUploadToolConfigurationOptions,
} from './types';
import { findScriptFile, hasSentryContent } from '../../utils/ast-utils';

import * as path from 'path';
import * as fs from 'fs';
import { debug } from '../../utils/debug';

const getCodeSnippet = (options: SourceMapUploadToolConfigurationOptions) =>
chalk.gray(`
Expand Down Expand Up @@ -48,21 +60,98 @@ export const configureVitePlugin: SourceMapUploadToolConfigurationFunction =
),
});

clack.log.step(
`Add the following code to your ${chalk.bold('vite.config.js')} file:`,
const viteConfigPath = findScriptFile(
path.resolve(process.cwd(), 'vite.config'),
);

// Intentially logging directly to console here so that the code can be copied/pasted directly
// eslint-disable-next-line no-console
console.log(getCodeSnippet(options));
let successfullyAdded = false;
if (viteConfigPath) {
successfullyAdded = await addVitePluginToConfig(viteConfigPath, options);
} else {
Sentry.setTag('ast-mod-fail-reason', 'config-not-found');
}

await abortIfCancelled(
select({
message: 'Did you copy the snippet above?',
options: [{ label: 'Yes, continue!', value: true }],
initialValue: true,
}),
);
if (successfullyAdded) {
Sentry.setTag('ast-mod', 'success');
} else {
Sentry.setTag('ast-mod', 'fail');
await showCopyPasteInstructions(
path.basename(viteConfigPath || 'vite.config.js'),
options,
);
}

await addDotEnvSentryBuildPluginFile(options.authToken);
};

async function addVitePluginToConfig(
viteConfigPath: string,
options: SourceMapUploadToolConfigurationOptions,
): Promise<boolean> {
try {
const prettyViteConfigFilename = chalk.cyan(path.basename(viteConfigPath));

const viteConfigContent = (
await fs.promises.readFile(viteConfigPath)
).toString();

const mod = parseModule(viteConfigContent);

if (hasSentryContent(mod)) {
clack.log.warn(
`File ${prettyViteConfigFilename} already contains Sentry code.
Please follow the instruction below`,
);
Sentry.setTag('ast-mod-fail-reason', 'has-sentry-content');
return false;
}

const { orgSlug: org, projectSlug: project, selfHosted, url } = options;

addVitePlugin(mod, {
imported: 'sentryVitePlugin',
from: '@sentry/vite-plugin',
constructor: 'sentryVitePlugin',
options: {
org,
project,
...(selfHosted && { url }),
},
});

const code = generateCode(mod.$ast).code;

await fs.promises.writeFile(viteConfigPath, code);

clack.log.success(
`Added the Sentry Vite plugin to ${prettyViteConfigFilename}`,
);

return true;
} catch (e) {
debug(e);
Sentry.setTag('ast-mod-fail-reason', 'insertion-fail');
return false;
}
}

async function showCopyPasteInstructions(
viteConfigFilename: string,
options: SourceMapUploadToolConfigurationOptions,
) {
clack.log.step(
`Add the following code to your ${chalk.cyan(viteConfigFilename)} file:`,
);

// Intentionally logging directly to console here so that the code can be copied/pasted directly
// eslint-disable-next-line no-console
console.log(getCodeSnippet(options));

await abortIfCancelled(
select({
message: 'Did you copy the snippet above?',
options: [{ label: 'Yes, continue!', value: true }],
initialValue: true,
}),
);
}
48 changes: 18 additions & 30 deletions src/sveltekit/sdk-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { addVitePlugin } from 'magicast/helpers';
import { getClientHooksTemplate, getServerHooksTemplate } from './templates';
import { abortIfCancelled, isUsingTypeScript } from '../utils/clack-utils';
import { debug } from '../utils/debug';
import { findScriptFile, hasSentryContent } from '../utils/ast-utils';

const SVELTE_CONFIG_FILE = 'svelte.config.js';

Expand Down Expand Up @@ -102,17 +103,6 @@ function getHooksConfigDirs(svelteConfig: PartialSvelteConfig): {
};
}

/**
* Checks if a JS/TS file where we don't know its concrete file type yet exists
* and returns the full path to the file with the correct file type.
*/
function findScriptFile(hooksFile: string): string | undefined {
const possibleFileTypes = ['.js', '.ts', '.mjs'];
return possibleFileTypes
.map((type) => `${hooksFile}${type}`)
.find((file) => fs.existsSync(file));
}

/**
* Reads the template, replaces the dsn placeholder with the actual dsn and writes the file to @param hooksFileDest
*/
Expand Down Expand Up @@ -149,9 +139,15 @@ async function mergeHooksFile(
dsn: string,
): Promise<void> {
const originalHooksMod = await loadFile(hooksFile);
if (hasSentryContent(path.basename(hooksFile), originalHooksMod.$code)) {
if (hasSentryContent(originalHooksMod)) {
// We don't want to mess with files that already have Sentry content.
// Let's just bail out at this point.
clack.log.warn(
`File ${chalk.cyan(
path.basename(hooksFile),
)} already contains Sentry code.
Skipping adding Sentry functionality to.`,
);
return;
}

Expand Down Expand Up @@ -359,20 +355,6 @@ function wrapHandle(mod: ProxifiedModule<any>): void {
}
}

/** Checks if the Sentry SvelteKit SDK is already mentioned in the file */
function hasSentryContent(fileName: string, fileContent: string): boolean {
if (fileContent.includes('@sentry/sveltekit')) {
clack.log.warn(
`File ${chalk.cyan(path.basename(fileName))} already contains Sentry code.
Skipping adding Sentry functionality to ${chalk.cyan(
path.basename(fileName),
)}.`,
);
return true;
}
return false;
}

export async function loadSvelteConfig(): Promise<PartialSvelteConfig> {
const configFilePath = path.join(process.cwd(), SVELTE_CONFIG_FILE);

Expand Down Expand Up @@ -412,15 +394,21 @@ async function modifyViteConfig(
await fs.promises.readFile(viteConfigPath, 'utf-8')
).toString();

if (hasSentryContent(viteConfigPath, viteConfigContent)) {
return;
}

const { org, project, url, selfHosted } = projectInfo;

try {
const viteModule = parseModule(viteConfigContent);

if (hasSentryContent(viteModule)) {
clack.log.warn(
`File ${chalk.cyan(
path.basename(viteConfigPath),
)} already contains Sentry code.
Skipping adding Sentry functionality to.`,
);
return;
}

addVitePlugin(viteModule, {
imported: 'sentrySvelteKit',
from: '@sentry/sveltekit',
Expand Down
20 changes: 20 additions & 0 deletions src/utils/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as fs from 'fs';
// @ts-ignore - magicast is ESM and TS complains about that. It works though
import { ProxifiedModule } from 'magicast';

/**
* Checks if a JS/TS file where we don't know its concrete file type yet exists
* and returns the full path to the file with the correct file type.
*/
export function findScriptFile(hooksFile: string): string | undefined {
const possibleFileTypes = ['.js', '.ts', '.mjs'];
return possibleFileTypes
.map((type) => `${hooksFile}${type}`)
.find((file) => fs.existsSync(file));
}

/** Checks if a Sentry package is already mentioned in the file */
export function hasSentryContent(mod: ProxifiedModule<object>): boolean {
const imports = mod.imports.$items.map((i) => i.from);
return !!imports.find((i) => i.startsWith('@sentry/'));
}
44 changes: 44 additions & 0 deletions test/utils/ast-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//@ts-ignore
import { parseModule } from 'magicast';
import { hasSentryContent } from '../../src/utils/ast-utils';

describe('AST utils', () => {
describe('hasSentryContent', () => {
it("returns true if a '@sentry/' import was found in the parsed module", () => {
const code = `
import { sentryVitePlugin } from "@sentry/vite-plugin";
import * as somethingelse from 'gs';
export default {
plugins: [sentryVitePlugin()]
}
`;

expect(hasSentryContent(parseModule(code))).toBe(true);
});
it.each([
`
import * as somethingelse from 'gs';
export default {
plugins: []
}
`,
`import * as somethingelse from 'gs';
// import { sentryVitePlugin } from "@sentry/vite-plugin"
export default {
plugins: []
}
`,
`import * as thirdPartyVitePlugin from "vite-plugin-@sentry"
export default {
plugins: [thirdPartyVitePlugin()]
}
`,
])(
"reutrns false for modules without a valid '@sentry/' import",
(code) => {
expect(hasSentryContent(parseModule(code))).toBe(false);
},
);
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"bin.ts",
"lib/**/*",
"spec/**/*",
"src/**/*"
"src/**/*",
"test/**/*"
]
}

0 comments on commit 6aac8b4

Please sign in to comment.