@sentry/wizard 3.9.2 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/lib/Constants.d.ts +1 -0
- package/dist/lib/Constants.js +5 -0
- package/dist/lib/Constants.js.map +1 -1
- package/dist/lib/Steps/ChooseIntegration.js +7 -0
- package/dist/lib/Steps/ChooseIntegration.js.map +1 -1
- package/dist/lib/Steps/Integrations/Cordova.js +5 -1
- package/dist/lib/Steps/Integrations/Cordova.js.map +1 -1
- package/dist/lib/Steps/Integrations/Remix.d.ts +12 -0
- package/dist/lib/Steps/Integrations/Remix.js +98 -0
- package/dist/lib/Steps/Integrations/Remix.js.map +1 -0
- package/dist/package.json +1 -1
- package/dist/src/remix/codemods/handle-error.d.ts +2 -0
- package/dist/src/remix/codemods/handle-error.js +70 -0
- package/dist/src/remix/codemods/handle-error.js.map +1 -0
- package/dist/src/remix/codemods/root-v1.d.ts +1 -0
- package/dist/src/remix/codemods/root-v1.js +133 -0
- package/dist/src/remix/codemods/root-v1.js.map +1 -0
- package/dist/src/remix/codemods/root-v2.d.ts +1 -0
- package/dist/src/remix/codemods/root-v2.js +134 -0
- package/dist/src/remix/codemods/root-v2.js.map +1 -0
- package/dist/src/remix/remix-wizard.d.ts +2 -0
- package/dist/src/remix/remix-wizard.js +206 -0
- package/dist/src/remix/remix-wizard.js.map +1 -0
- package/dist/src/remix/sdk-setup.d.ts +18 -0
- package/dist/src/remix/sdk-setup.js +293 -0
- package/dist/src/remix/sdk-setup.js.map +1 -0
- package/dist/src/remix/templates.d.ts +2 -0
- package/dist/src/remix/templates.js +6 -0
- package/dist/src/remix/templates.js.map +1 -0
- package/dist/src/remix/utils.d.ts +6 -0
- package/dist/src/remix/utils.js +55 -0
- package/dist/src/remix/utils.js.map +1 -0
- package/dist/src/sourcemaps/sourcemaps-wizard.js +23 -12
- package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
- package/dist/src/sourcemaps/tools/remix.d.ts +3 -0
- package/dist/src/sourcemaps/tools/remix.js +125 -0
- package/dist/src/sourcemaps/tools/remix.js.map +1 -0
- package/dist/src/sourcemaps/utils/detect-tool.d.ts +1 -1
- package/dist/src/sourcemaps/utils/detect-tool.js +1 -0
- package/dist/src/sourcemaps/utils/detect-tool.js.map +1 -1
- package/dist/src/utils/clack-utils.d.ts +3 -2
- package/dist/src/utils/clack-utils.js +39 -2
- package/dist/src/utils/clack-utils.js.map +1 -1
- package/lib/Constants.ts +5 -0
- package/lib/Steps/ChooseIntegration.ts +7 -0
- package/lib/Steps/Integrations/Cordova.ts +5 -1
- package/lib/Steps/Integrations/Remix.ts +32 -0
- package/package.json +1 -1
- package/src/remix/codemods/handle-error.ts +67 -0
- package/src/remix/codemods/root-v1.ts +91 -0
- package/src/remix/codemods/root-v2.ts +84 -0
- package/src/remix/remix-wizard.ts +137 -0
- package/src/remix/sdk-setup.ts +300 -0
- package/src/remix/templates.ts +15 -0
- package/src/remix/utils.ts +41 -0
- package/src/sourcemaps/sourcemaps-wizard.ts +9 -0
- package/src/sourcemaps/tools/remix.ts +90 -0
- package/src/sourcemaps/utils/detect-tool.ts +3 -1
- package/src/utils/clack-utils.ts +56 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
|
|
3
|
+
import * as recast from 'recast';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
// @ts-expect-error - clack is ESM and TS complains about that. It works though
|
|
7
|
+
import clack from '@clack/prompts';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
|
|
11
|
+
import { builders, generateCode, loadFile, writeFile } from 'magicast';
|
|
12
|
+
|
|
13
|
+
export async function instrumentRootRouteV1(
|
|
14
|
+
rootFileName: string,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
const rootRouteAst = await loadFile(
|
|
18
|
+
path.join(process.cwd(), 'app', rootFileName),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
rootRouteAst.imports.$add({
|
|
22
|
+
from: '@sentry/remix',
|
|
23
|
+
imported: 'withSentry',
|
|
24
|
+
local: 'withSentry',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
recast.visit(rootRouteAst.$ast, {
|
|
28
|
+
visitExportDefaultDeclaration(path) {
|
|
29
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
30
|
+
if (path.value.declaration.type === 'FunctionDeclaration') {
|
|
31
|
+
// Move the function declaration just before the default export
|
|
32
|
+
path.insertBefore(path.value.declaration);
|
|
33
|
+
|
|
34
|
+
// Get the name of the function to be wrapped
|
|
35
|
+
const functionName: string = path.value.declaration.id.name as string;
|
|
36
|
+
|
|
37
|
+
// Create the wrapped function call
|
|
38
|
+
const functionCall = recast.types.builders.callExpression(
|
|
39
|
+
recast.types.builders.identifier('withSentry'),
|
|
40
|
+
[recast.types.builders.identifier(functionName)],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Replace the default export with the wrapped function call
|
|
44
|
+
path.value.declaration = functionCall;
|
|
45
|
+
} else if (path.value.declaration.type === 'Identifier') {
|
|
46
|
+
const rootRouteExport = rootRouteAst.exports.default;
|
|
47
|
+
|
|
48
|
+
const expressionToWrap = generateCode(
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
50
|
+
rootRouteExport.$ast,
|
|
51
|
+
).code;
|
|
52
|
+
|
|
53
|
+
rootRouteAst.exports.default = builders.raw(
|
|
54
|
+
`withSentry(${expressionToWrap})`,
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
clack.log.warn(
|
|
58
|
+
chalk.yellow(
|
|
59
|
+
`Couldn't instrument ${chalk.bold(
|
|
60
|
+
rootFileName,
|
|
61
|
+
)} automatically. Wrap your default export with: ${chalk.dim(
|
|
62
|
+
'withSentry()',
|
|
63
|
+
)}\n`,
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.traverse(path);
|
|
69
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await writeFile(
|
|
74
|
+
rootRouteAst.$ast,
|
|
75
|
+
path.join(process.cwd(), 'app', rootFileName),
|
|
76
|
+
);
|
|
77
|
+
} catch (e: unknown) {
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.error(e);
|
|
80
|
+
clack.log.warn(
|
|
81
|
+
chalk.yellow(
|
|
82
|
+
`Something went wrong writing to ${chalk.bold(rootFileName)}`,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
clack.log.info(
|
|
86
|
+
`Please put the following code snippet into ${chalk.bold(
|
|
87
|
+
rootFileName,
|
|
88
|
+
)}: ${chalk.dim('You probably have to clean it up a bit.')}\n`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
|
|
3
|
+
import * as recast from 'recast';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
import type { ExportNamedDeclaration, Program } from '@babel/types';
|
|
7
|
+
|
|
8
|
+
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
|
|
9
|
+
import { loadFile, writeFile } from 'magicast';
|
|
10
|
+
|
|
11
|
+
import { ERROR_BOUNDARY_TEMPLATE_V2 } from '../templates';
|
|
12
|
+
|
|
13
|
+
export async function instrumentRootRouteV2(
|
|
14
|
+
rootFileName: string,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const rootRouteAst = await loadFile(
|
|
17
|
+
path.join(process.cwd(), 'app', rootFileName),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const exportsAst = rootRouteAst.exports.$ast as Program;
|
|
21
|
+
|
|
22
|
+
const namedExports = exportsAst.body.filter(
|
|
23
|
+
(node) => node.type === 'ExportNamedDeclaration',
|
|
24
|
+
) as ExportNamedDeclaration[];
|
|
25
|
+
|
|
26
|
+
let foundErrorBoundary = false;
|
|
27
|
+
|
|
28
|
+
namedExports.forEach((namedExport) => {
|
|
29
|
+
const declaration = namedExport.declaration;
|
|
30
|
+
|
|
31
|
+
if (!declaration) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (declaration.type === 'FunctionDeclaration') {
|
|
36
|
+
if (declaration.id?.name === 'ErrorBoundary') {
|
|
37
|
+
foundErrorBoundary = true;
|
|
38
|
+
}
|
|
39
|
+
} else if (declaration.type === 'VariableDeclaration') {
|
|
40
|
+
const declarations = declaration.declarations;
|
|
41
|
+
|
|
42
|
+
declarations.forEach((declaration) => {
|
|
43
|
+
// @ts-expect-error - id should always have a name in this case
|
|
44
|
+
if (declaration.id?.name === 'ErrorBoundary') {
|
|
45
|
+
foundErrorBoundary = true;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!foundErrorBoundary) {
|
|
52
|
+
rootRouteAst.imports.$add({
|
|
53
|
+
from: '@sentry/remix',
|
|
54
|
+
imported: 'captureRemixErrorBoundaryError',
|
|
55
|
+
local: 'captureRemixErrorBoundaryError',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
rootRouteAst.imports.$add({
|
|
59
|
+
from: '@remix-run/react',
|
|
60
|
+
imported: 'useRouteError',
|
|
61
|
+
local: 'useRouteError',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
recast.visit(rootRouteAst.$ast, {
|
|
65
|
+
visitExportDefaultDeclaration(path) {
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
67
|
+
const implementation = recast.parse(ERROR_BOUNDARY_TEMPLATE_V2).program
|
|
68
|
+
.body[0];
|
|
69
|
+
|
|
70
|
+
path.insertBefore(
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
72
|
+
recast.types.builders.exportDeclaration(false, implementation),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
this.traverse(path);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await writeFile(
|
|
81
|
+
rootRouteAst.$ast,
|
|
82
|
+
path.join(process.cwd(), 'app', rootFileName),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @ts-expect-error - clack is ESM and TS complains about that. It works though
|
|
2
|
+
import clack from '@clack/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
addSentryCliRc,
|
|
7
|
+
askForProjectSelection,
|
|
8
|
+
askForSelfHosted,
|
|
9
|
+
askForWizardLogin,
|
|
10
|
+
confirmContinueEvenThoughNoGitRepo,
|
|
11
|
+
ensurePackageIsInstalled,
|
|
12
|
+
getPackageDotJson,
|
|
13
|
+
installPackage,
|
|
14
|
+
isUsingTypeScript,
|
|
15
|
+
printWelcome,
|
|
16
|
+
} from '../utils/clack-utils';
|
|
17
|
+
import { hasPackageInstalled } from '../utils/package-json';
|
|
18
|
+
import { WizardOptions } from '../utils/types';
|
|
19
|
+
import {
|
|
20
|
+
initializeSentryOnEntryClient,
|
|
21
|
+
initializeSentryOnEntryServer,
|
|
22
|
+
updateBuildScript,
|
|
23
|
+
instrumentRootRoute,
|
|
24
|
+
isRemixV2,
|
|
25
|
+
loadRemixConfig,
|
|
26
|
+
} from './sdk-setup';
|
|
27
|
+
import { debug } from '../utils/debug';
|
|
28
|
+
import { traceStep, withTelemetry } from '../telemetry';
|
|
29
|
+
|
|
30
|
+
export async function runRemixWizard(options: WizardOptions): Promise<void> {
|
|
31
|
+
return withTelemetry(
|
|
32
|
+
{
|
|
33
|
+
enabled: options.telemetryEnabled,
|
|
34
|
+
integration: 'remix',
|
|
35
|
+
},
|
|
36
|
+
() => runRemixWizardWithTelemetry(options),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runRemixWizardWithTelemetry(
|
|
41
|
+
options: WizardOptions,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
printWelcome({
|
|
44
|
+
wizardName: 'Sentry Remix Wizard',
|
|
45
|
+
promoCode: options.promoCode,
|
|
46
|
+
telemetryEnabled: options.telemetryEnabled,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await confirmContinueEvenThoughNoGitRepo();
|
|
50
|
+
|
|
51
|
+
const remixConfig = await loadRemixConfig();
|
|
52
|
+
const packageJson = await getPackageDotJson();
|
|
53
|
+
|
|
54
|
+
// We expect `@remix-run/dev` to be installed for every Remix project
|
|
55
|
+
await ensurePackageIsInstalled(packageJson, '@remix-run/dev', 'Remix');
|
|
56
|
+
|
|
57
|
+
const { url: sentryUrl } = await askForSelfHosted(options.url);
|
|
58
|
+
|
|
59
|
+
const { projects, apiKeys } = await askForWizardLogin({
|
|
60
|
+
promoCode: options.promoCode,
|
|
61
|
+
url: sentryUrl,
|
|
62
|
+
platform: 'javascript-remix',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const selectedProject = await askForProjectSelection(projects);
|
|
66
|
+
|
|
67
|
+
await traceStep('Install Sentry SDK', () =>
|
|
68
|
+
installPackage({
|
|
69
|
+
packageName: '@sentry/remix',
|
|
70
|
+
alreadyInstalled: hasPackageInstalled('@sentry/remix', packageJson),
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const dsn = selectedProject.keys[0].dsn.public;
|
|
75
|
+
|
|
76
|
+
const isTS = isUsingTypeScript();
|
|
77
|
+
const isV2 = isRemixV2(remixConfig, packageJson);
|
|
78
|
+
|
|
79
|
+
await addSentryCliRc(
|
|
80
|
+
apiKeys.token,
|
|
81
|
+
selectedProject.organization.slug,
|
|
82
|
+
selectedProject.name,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await traceStep('Update build script for sourcemap uploads', async () => {
|
|
86
|
+
try {
|
|
87
|
+
await updateBuildScript();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
clack.log
|
|
90
|
+
.warn(`Could not update build script to generate and upload sourcemaps.
|
|
91
|
+
Please update your build script manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/sourcemaps/`);
|
|
92
|
+
debug(e);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await traceStep('Instrument root route', async () => {
|
|
97
|
+
try {
|
|
98
|
+
await instrumentRootRoute(isV2, isTS);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
clack.log.warn(`Could not instrument root route.
|
|
101
|
+
Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/`);
|
|
102
|
+
debug(e);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await traceStep('Initialize Sentry on client entry', async () => {
|
|
107
|
+
try {
|
|
108
|
+
await initializeSentryOnEntryClient(dsn, isTS);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
clack.log.warn(`Could not initialize Sentry on client entry.
|
|
111
|
+
Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/`);
|
|
112
|
+
debug(e);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await traceStep('Initialize Sentry on server entry', async () => {
|
|
117
|
+
try {
|
|
118
|
+
await initializeSentryOnEntryServer(dsn, isTS, isV2);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
clack.log.warn(`Could not initialize Sentry on server entry.
|
|
121
|
+
Please do it manually using instructions from https://docs.sentry.io/platforms/javascript/guides/remix/`);
|
|
122
|
+
debug(e);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
clack.outro(`
|
|
127
|
+
${chalk.green(
|
|
128
|
+
'Sentry has been successfully configured for your Remix project.',
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
${chalk.cyan('You can now deploy your project to see Sentry in action.')}
|
|
132
|
+
|
|
133
|
+
${chalk.cyan(
|
|
134
|
+
`To learn more about how to use Sentry with Remix, visit our documentation:
|
|
135
|
+
https://docs.sentry.io/platforms/javascript/guides/remix/`,
|
|
136
|
+
)}`);
|
|
137
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
|
|
3
|
+
import type { Program } from '@babel/types';
|
|
4
|
+
|
|
5
|
+
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
|
|
6
|
+
import type { ProxifiedModule } from 'magicast';
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as url from 'url';
|
|
11
|
+
|
|
12
|
+
// @ts-expect-error - clack is ESM and TS complains about that. It works though
|
|
13
|
+
import clack from '@clack/prompts';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { parse } from 'semver';
|
|
16
|
+
|
|
17
|
+
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
|
|
18
|
+
import { builders, generateCode, loadFile, writeFile } from 'magicast';
|
|
19
|
+
import { PackageDotJson, getPackageVersion } from '../utils/package-json';
|
|
20
|
+
import { getInitCallInsertionIndex, hasSentryContent } from './utils';
|
|
21
|
+
import { instrumentRootRouteV1 } from './codemods/root-v1';
|
|
22
|
+
import { instrumentRootRouteV2 } from './codemods/root-v2';
|
|
23
|
+
import { instrumentHandleError } from './codemods/handle-error';
|
|
24
|
+
|
|
25
|
+
export type PartialRemixConfig = {
|
|
26
|
+
unstable_dev?: boolean;
|
|
27
|
+
future?: {
|
|
28
|
+
v2_dev?: boolean;
|
|
29
|
+
v2_errorBoundary?: boolean;
|
|
30
|
+
v2_headers?: boolean;
|
|
31
|
+
v2_meta?: boolean;
|
|
32
|
+
v2_normalizeFormMethod?: boolean;
|
|
33
|
+
v2_routeConvention?: boolean;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const REMIX_CONFIG_FILE = 'remix.config.js';
|
|
38
|
+
|
|
39
|
+
function insertClientInitCall(
|
|
40
|
+
dsn: string,
|
|
41
|
+
originalHooksMod: ProxifiedModule<any>,
|
|
42
|
+
): void {
|
|
43
|
+
const initCall = builders.functionCall('Sentry.init', {
|
|
44
|
+
dsn,
|
|
45
|
+
tracesSampleRate: 1.0,
|
|
46
|
+
replaysSessionSampleRate: 0.1,
|
|
47
|
+
replaysOnErrorSampleRate: 1.0,
|
|
48
|
+
integrations: [
|
|
49
|
+
builders.newExpression('Sentry.BrowserTracing', {
|
|
50
|
+
routingInstrumentation: builders.functionCall(
|
|
51
|
+
'Sentry.remixRouterInstrumentation',
|
|
52
|
+
builders.raw('useEffect'),
|
|
53
|
+
builders.raw('useLocation'),
|
|
54
|
+
builders.raw('useMatches'),
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
builders.newExpression('Sentry.Replay'),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const originalHooksModAST = originalHooksMod.$ast as Program;
|
|
62
|
+
const initCallInsertionIndex = getInitCallInsertionIndex(originalHooksModAST);
|
|
63
|
+
|
|
64
|
+
originalHooksModAST.body.splice(
|
|
65
|
+
initCallInsertionIndex,
|
|
66
|
+
0,
|
|
67
|
+
// @ts-expect-error - string works here because the AST is proxified by magicast
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
69
|
+
generateCode(initCall).code,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function insertServerInitCall(
|
|
74
|
+
dsn: string,
|
|
75
|
+
originalHooksMod: ProxifiedModule<any>,
|
|
76
|
+
) {
|
|
77
|
+
const initCall = builders.functionCall('Sentry.init', {
|
|
78
|
+
dsn,
|
|
79
|
+
tracesSampleRate: 1.0,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const originalHooksModAST = originalHooksMod.$ast as Program;
|
|
83
|
+
|
|
84
|
+
const initCallInsertionIndex = getInitCallInsertionIndex(originalHooksModAST);
|
|
85
|
+
|
|
86
|
+
originalHooksModAST.body.splice(
|
|
87
|
+
initCallInsertionIndex,
|
|
88
|
+
0,
|
|
89
|
+
// @ts-expect-error - string works here because the AST is proxified by magicast
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
91
|
+
generateCode(initCall).code,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isRemixV2(
|
|
96
|
+
remixConfig: PartialRemixConfig,
|
|
97
|
+
packageJson: PackageDotJson,
|
|
98
|
+
): boolean {
|
|
99
|
+
const remixVersion = getPackageVersion('@remix-run/react', packageJson);
|
|
100
|
+
const remixVersionMajor = remixVersion && parse(remixVersion)?.major;
|
|
101
|
+
const isV2Remix = remixVersionMajor && remixVersionMajor >= 2;
|
|
102
|
+
|
|
103
|
+
return isV2Remix || remixConfig?.future?.v2_errorBoundary || false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function loadRemixConfig(): Promise<PartialRemixConfig> {
|
|
107
|
+
const configFilePath = path.join(process.cwd(), REMIX_CONFIG_FILE);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (!fs.existsSync(configFilePath)) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const configUrl = url.pathToFileURL(configFilePath).href;
|
|
115
|
+
const remixConfigModule = (await import(configUrl)) as {
|
|
116
|
+
default: PartialRemixConfig;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return remixConfigModule?.default || {};
|
|
120
|
+
} catch (e: unknown) {
|
|
121
|
+
clack.log.error(`Couldn't load ${REMIX_CONFIG_FILE}.`);
|
|
122
|
+
clack.log.info(
|
|
123
|
+
chalk.dim(
|
|
124
|
+
typeof e === 'object' && e != null && 'toString' in e
|
|
125
|
+
? e.toString()
|
|
126
|
+
: typeof e === 'string'
|
|
127
|
+
? e
|
|
128
|
+
: 'Unknown error',
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function instrumentRootRoute(
|
|
137
|
+
isV2?: boolean,
|
|
138
|
+
isTS?: boolean,
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
const rootFilename = `root.${isTS ? 'tsx' : 'jsx'}`;
|
|
141
|
+
|
|
142
|
+
if (isV2) {
|
|
143
|
+
await instrumentRootRouteV2(rootFilename);
|
|
144
|
+
} else {
|
|
145
|
+
await instrumentRootRouteV1(rootFilename);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
clack.log.success(
|
|
149
|
+
`Successfully instrumented root route ${chalk.cyan(rootFilename)}.`,
|
|
150
|
+
);
|
|
151
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function updateBuildScript(): Promise<void> {
|
|
155
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
156
|
+
// Add sourcemaps option to build script
|
|
157
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
158
|
+
const packageJsonString = (
|
|
159
|
+
await fs.promises.readFile(packageJsonPath)
|
|
160
|
+
).toString();
|
|
161
|
+
const packageJson = JSON.parse(packageJsonString);
|
|
162
|
+
|
|
163
|
+
if (!packageJson.scripts) {
|
|
164
|
+
packageJson.scripts = {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!packageJson.scripts.build) {
|
|
168
|
+
packageJson.scripts.build =
|
|
169
|
+
'remix build --sourcemap && sentry-upload-sourcemaps';
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
171
|
+
} else if (packageJson.scripts.build.includes('remix build')) {
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
173
|
+
packageJson.scripts.build = packageJson.scripts.build.replace(
|
|
174
|
+
'remix build',
|
|
175
|
+
'remix build --sourcemap && sentry-upload-sourcemaps',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await fs.promises.writeFile(
|
|
180
|
+
packageJsonPath,
|
|
181
|
+
JSON.stringify(packageJson, null, 2),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
clack.log.success(
|
|
185
|
+
`Successfully updated ${chalk.cyan('build')} script in ${chalk.cyan(
|
|
186
|
+
'package.json',
|
|
187
|
+
)} to generate and upload sourcemaps.`,
|
|
188
|
+
);
|
|
189
|
+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function initializeSentryOnEntryClient(
|
|
193
|
+
dsn: string,
|
|
194
|
+
isTS: boolean,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
const clientEntryFilename = `entry.client.${isTS ? 'tsx' : 'jsx'}`;
|
|
197
|
+
|
|
198
|
+
const originalEntryClient = path.join(
|
|
199
|
+
process.cwd(),
|
|
200
|
+
'app',
|
|
201
|
+
clientEntryFilename,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const originalEntryClientMod = await loadFile(originalEntryClient);
|
|
205
|
+
|
|
206
|
+
if (hasSentryContent(originalEntryClient, originalEntryClientMod.$code)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
originalEntryClientMod.imports.$add({
|
|
211
|
+
from: '@sentry/remix',
|
|
212
|
+
imported: '*',
|
|
213
|
+
local: 'Sentry',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
originalEntryClientMod.imports.$add({
|
|
217
|
+
from: 'react',
|
|
218
|
+
imported: 'useEffect',
|
|
219
|
+
local: 'useEffect',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
originalEntryClientMod.imports.$add({
|
|
223
|
+
from: '@remix-run/react',
|
|
224
|
+
imported: 'useLocation',
|
|
225
|
+
local: 'useLocation',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
originalEntryClientMod.imports.$add({
|
|
229
|
+
from: '@remix-run/react',
|
|
230
|
+
imported: 'useMatches',
|
|
231
|
+
local: 'useMatches',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
insertClientInitCall(dsn, originalEntryClientMod);
|
|
235
|
+
|
|
236
|
+
await writeFile(
|
|
237
|
+
originalEntryClientMod.$ast,
|
|
238
|
+
path.join(process.cwd(), 'app', clientEntryFilename),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
clack.log.success(
|
|
242
|
+
`Successfully initialized Sentry on client entry point ${chalk.cyan(
|
|
243
|
+
clientEntryFilename,
|
|
244
|
+
)}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function initializeSentryOnEntryServer(
|
|
249
|
+
dsn: string,
|
|
250
|
+
isV2: boolean,
|
|
251
|
+
isTS: boolean,
|
|
252
|
+
): Promise<void> {
|
|
253
|
+
const serverEntryFilename = `entry.server.${isTS ? 'tsx' : 'jsx'}`;
|
|
254
|
+
|
|
255
|
+
const originalEntryServer = path.join(
|
|
256
|
+
process.cwd(),
|
|
257
|
+
'app',
|
|
258
|
+
serverEntryFilename,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const originalEntryServerMod = await loadFile(originalEntryServer);
|
|
262
|
+
|
|
263
|
+
if (hasSentryContent(originalEntryServer, originalEntryServerMod.$code)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
originalEntryServerMod.imports.$add({
|
|
268
|
+
from: '@sentry/remix',
|
|
269
|
+
imported: '*',
|
|
270
|
+
local: 'Sentry',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
insertServerInitCall(dsn, originalEntryServerMod);
|
|
274
|
+
|
|
275
|
+
if (isV2) {
|
|
276
|
+
const handleErrorInstrumented = instrumentHandleError(
|
|
277
|
+
originalEntryServerMod,
|
|
278
|
+
serverEntryFilename,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (handleErrorInstrumented) {
|
|
282
|
+
clack.log.success(
|
|
283
|
+
`Instrumented ${chalk.cyan('handleError')} in ${chalk.cyan(
|
|
284
|
+
`${serverEntryFilename}`,
|
|
285
|
+
)}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await writeFile(
|
|
291
|
+
originalEntryServerMod.$ast,
|
|
292
|
+
path.join(process.cwd(), 'app', serverEntryFilename),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
clack.log.success(
|
|
296
|
+
`Successfully initialized Sentry on server entry point ${chalk.cyan(
|
|
297
|
+
serverEntryFilename,
|
|
298
|
+
)}.`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const ERROR_BOUNDARY_TEMPLATE_V2 = `const ErrorBoundary = () => {
|
|
2
|
+
const error = useRouteError();
|
|
3
|
+
captureRemixErrorBoundaryError(error);
|
|
4
|
+
return <div>Something went wrong</div>;
|
|
5
|
+
};
|
|
6
|
+
`;
|
|
7
|
+
|
|
8
|
+
export const HANDLE_ERROR_TEMPLATE_V2 = `function handleError(error) {
|
|
9
|
+
if (error instanceof Error) {
|
|
10
|
+
Sentry.captureRemixErrorBoundaryError(error);
|
|
11
|
+
} else {
|
|
12
|
+
Sentry.captureException(error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Program } from '@babel/types';
|
|
2
|
+
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
// @ts-expect-error - clack is ESM and TS complains about that. It works though
|
|
6
|
+
import clack from '@clack/prompts';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
// Copied from sveltekit wizard
|
|
10
|
+
export function hasSentryContent(
|
|
11
|
+
fileName: string,
|
|
12
|
+
fileContent: string,
|
|
13
|
+
): boolean {
|
|
14
|
+
const includesContent = fileContent.includes('@sentry/remix');
|
|
15
|
+
|
|
16
|
+
if (includesContent) {
|
|
17
|
+
clack.log.warn(
|
|
18
|
+
`File ${chalk.cyan(path.basename(fileName))} already contains Sentry code.
|
|
19
|
+
Skipping adding Sentry functionality to ${chalk.cyan(
|
|
20
|
+
path.basename(fileName),
|
|
21
|
+
)}.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return includesContent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* We want to insert the init call on top of the file but after all import statements
|
|
30
|
+
*/
|
|
31
|
+
export function getInitCallInsertionIndex(
|
|
32
|
+
originalHooksModAST: Program,
|
|
33
|
+
): number {
|
|
34
|
+
for (let x = originalHooksModAST.body.length - 1; x >= 0; x--) {
|
|
35
|
+
if (originalHooksModAST.body[x].type === 'ImportDeclaration') {
|
|
36
|
+
return x + 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
@@ -29,6 +29,7 @@ import { checkIfMoreSuitableWizardExistsAndAskForRedirect } from './utils/other-
|
|
|
29
29
|
import { configureAngularSourcemapGenerationFlow } from './tools/angular';
|
|
30
30
|
import { detectUsedTool, SupportedTools } from './utils/detect-tool';
|
|
31
31
|
import { configureNextJsSourceMapsUpload } from './tools/nextjs';
|
|
32
|
+
import { configureRemixSourceMapsUpload } from './tools/remix';
|
|
32
33
|
|
|
33
34
|
export async function runSourcemapsWizard(
|
|
34
35
|
options: WizardOptions,
|
|
@@ -133,6 +134,11 @@ async function askForUsedBundlerTool(): Promise<SupportedTools> {
|
|
|
133
134
|
value: 'nextjs',
|
|
134
135
|
hint: 'Select this option if you want to set up source maps in a NextJS project.',
|
|
135
136
|
},
|
|
137
|
+
{
|
|
138
|
+
label: 'Remix',
|
|
139
|
+
value: 'remix',
|
|
140
|
+
hint: 'Select this option if you want to set up source maps in a Remix project.',
|
|
141
|
+
},
|
|
136
142
|
{
|
|
137
143
|
label: 'Webpack',
|
|
138
144
|
value: 'webpack',
|
|
@@ -204,6 +210,9 @@ async function startToolSetupFlow(
|
|
|
204
210
|
case 'nextjs':
|
|
205
211
|
await configureNextJsSourceMapsUpload(options, wizardOptions);
|
|
206
212
|
break;
|
|
213
|
+
case 'remix':
|
|
214
|
+
await configureRemixSourceMapsUpload(options, wizardOptions);
|
|
215
|
+
break;
|
|
207
216
|
default:
|
|
208
217
|
await configureSentryCLI(options);
|
|
209
218
|
break;
|