Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
feat: Add transform to extract node config into separate file (#41)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukas Stracke <[email protected]>
  • Loading branch information
mydea and Lms24 authored May 13, 2024
1 parent 3d74730 commit 449bef0
Show file tree
Hide file tree
Showing 16 changed files with 593 additions and 3 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ You can run `npx @sentry/migr8 --help` to get a list of available options.

## Transformations

## Move @sentry/node config into instrument.js file

When using `@sentry/node` in v8, it is required to call `Sentry.init()` before anything else is required/imported.
Because of this, the recommended way to initialize Sentry is in a separate file (e.g. `instrument.js`) which is
required/imported at the very top of your application file.

This transform will try to detect this case and create a standalone instrumentation file for you.

### Add migration comments

There are certain things that migr8 cannot auto-fix for you. This is the case for things like `startTransaction()`,
Expand Down
14 changes: 11 additions & 3 deletions src/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,22 @@ export async function runWithTelementry(options) {

await traceStep('run-transformers', async () => {
for (const transformer of transformers) {
const showSpinner = !transformer.requiresUserInput;

const s = spinner();
s.start(`Running transformer "${transformer.name}"...`);
if (showSpinner) {
s.start(`Running transformer "${transformer.name}"...`);
}

try {
await traceStep(transformer.name, () => transformer.transform(files, { ...options, sdk: targetSdk }));
s.stop(`Transformer "${transformer.name}" completed.`);
if (showSpinner) {
s.stop(`Transformer "${transformer.name}" completed.`);
}
} catch (error) {
s.stop(`Transformer "${transformer.name}" failed to complete with error:`);
if (showSpinner) {
s.stop(`Transformer "${transformer.name}" failed to complete with error:`);
}
// eslint-disable-next-line no-console
console.error(error);
}
Expand Down
47 changes: 47 additions & 0 deletions src/transformers/nodeInstrumentFile/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import path from 'path';
import url from 'url';

import { log } from '@clack/prompts';
import chalk from 'chalk';

import { runJscodeshift } from '../../utils/jscodeshift.js';

/**
* @type {import('types').Transformer}
*/
export default {
name: 'Move @sentry/node config into instrument.js file',
transform: async (files, options) => {
if (options.sdk !== '@sentry/node') {
// this transform only applies to the Node SDK
return;
}

log.info(`Moving your Sentry config into a dedicated ${chalk.cyan('instrument.js')} file.
In version 8, the order of importing and initializing Sentry matters a lot.
We therefore move your Sentry initialization into a separate file and import it right on top of your file.
This ensures correct ordering.
More information: ${chalk.gray('https://docs.sentry.io/platforms/javascript/guides/node/#configure')}`);

log.info(
`${chalk.underline(
'Important:'
)} If you're using EcmaScript Modules (ESM) mode, you need to load the ${chalk.cyan(
'instrument.js'
)} file differently.
Please read this guide: ${chalk.gray('https://docs.sentry.io/platforms/javascript/guides/node/install/esm')}
After you've done that, remove import of ${chalk.cyan('instrument.js')} from your file(s).
This only applies if you are actually using ESM natively with Node.
If you use ${chalk.gray('import')} syntax but transpile to CommonJS (e.g. TypeScript), you're all set up already!
`
);

const transformPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), './transform.cjs');
await runJscodeshift(transformPath, files, options);
},
};
194 changes: 194 additions & 0 deletions src/transformers/nodeInstrumentFile/nodeInstrumentFile.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { rmSync } from 'node:fs';
import { beforeEach } from 'node:test';

import { afterEach, describe, it, expect, vi } from 'vitest';

import { fileExists, getDirFileContent, getFixturePath, makeTmpDir } from '../../../test-helpers/testPaths.js';

import tracingConfigTransformer from './index.js';

describe('transformers | nodeInstrumentFile', () => {
let tmpDir = '';

beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
if (tmpDir) {
rmSync(tmpDir, { force: true, recursive: true });
tmpDir = '';
}
});

it('has correct name', () => {
expect(tracingConfigTransformer.name).toEqual('Move @sentry/node config into instrument.js file');
});

it('works with app without Sentry', async () => {
tmpDir = makeTmpDir(getFixturePath('noSentry'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.js');
expect(actual1).toEqual(getDirFileContent(`${process.cwd()}/test-fixtures/noSentry`, 'app.js'));
});

it('works with browser SDK', async () => {
tmpDir = makeTmpDir(getFixturePath('nodeInstrumentFile/browserApp'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.js');
expect(actual1).toEqual(
getDirFileContent(`${process.cwd()}/test-fixtures/nodeInstrumentFile/browserApp`, 'app.js')
);
expect(fileExists(tmpDir, 'instrument.js')).toBe(false);
});

it('works with existing tracing.js', async () => {
tmpDir = makeTmpDir(getFixturePath('nodeInstrumentFile/nodeAppTracingFile'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.js');
const actual2 = getDirFileContent(tmpDir, 'tracing.js');
expect(actual1).toEqual(
getDirFileContent(`${process.cwd()}/test-fixtures/nodeInstrumentFile/nodeAppTracingFile`, 'app.js')
);
expect(actual2).toEqual(
getDirFileContent(`${process.cwd()}/test-fixtures/nodeInstrumentFile/nodeAppTracingFile`, 'tracing.js')
);
});

it('works with node SDK & simple import', async () => {
tmpDir = makeTmpDir(getFixturePath('nodeInstrumentFile/nodeAppImport'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.mjs');
expect(actual1).toEqual(
`import "./instrument";
// do something now!
console.log('Hello, World!');
`
);
expect(fileExists(tmpDir, 'instrument.mjs')).toBe(true);
expect(getDirFileContent(tmpDir, 'instrument.mjs')).toEqual(
`import * as Sentry from '@sentry/node';
Sentry.init({
dsn: 'https://example.com',
});`
);
});

it('works with node SDK & other imports', async () => {
tmpDir = makeTmpDir(getFixturePath('nodeInstrumentFile/nodeAppImports'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.js');
expect(actual1).toEqual(
`import "./instrument";
import { setTag } from '@sentry/node';
import { somethingElse } from 'other-package';
setTag('key', 'value');
// do something now!
console.log('Hello, World!', somethingElse.doThis());
`
);
expect(fileExists(tmpDir, 'instrument.js')).toBe(true);
expect(getDirFileContent(tmpDir, 'instrument.js')).toEqual(
`import { init, getActiveSpan } from '@sentry/node';
import { something } from 'other-package';
init({
dsn: 'https://example.com',
beforeSend(event) {
event.extra.hasSpan = !!getActiveSpan();
event.extra.check = something();
return event;
}
});`
);
});

it('works with node SDK & simple require', async () => {
tmpDir = makeTmpDir(getFixturePath('nodeInstrumentFile/nodeAppRequire'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.js');
expect(actual1).toEqual(
`require("./instrument");
// do something now!
console.log('Hello, World!');
`
);
expect(fileExists(tmpDir, 'instrument.js')).toBe(true);
expect(getDirFileContent(tmpDir, 'instrument.js')).toEqual(
`const Sentry = require('@sentry/node');
Sentry.init({
dsn: 'https://example.com',
});`
);
});

it('works with node SDK & other requires', async () => {
tmpDir = makeTmpDir(getFixturePath('nodeInstrumentFile/nodeAppRequires'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.js');
expect(actual1).toEqual(
`require("./instrument");
const {
setTag
} = require('@sentry/node');
const {
somethingElse
} = require('other-package');
setTag('key', 'value');
// do something now!
console.log('Hello, World!', somethingElse.doThis());
`
);
expect(fileExists(tmpDir, 'instrument.js')).toBe(true);
expect(getDirFileContent(tmpDir, 'instrument.js')).toEqual(
`const {
init,
getActiveSpan
} = require('@sentry/node');
const {
something
} = require('other-package');
init({
dsn: 'https://example.com',
beforeSend(event) {
event.extra.hasSpan = !!getActiveSpan();
event.extra.check = something();
return event;
}
});`
);
});

it('works with node SDK & typescript', async () => {
tmpDir = makeTmpDir(getFixturePath('nodeInstrumentFile/nodeAppTypescript'));
await tracingConfigTransformer.transform([tmpDir], { filePatterns: [], sdk: '@sentry/node' });

const actual1 = getDirFileContent(tmpDir, 'app.ts');
expect(actual1).toEqual(
`import "./instrument";
// do something now!
console.log('Hello, World!');
`
);
expect(fileExists(tmpDir, 'instrument.ts')).toBe(true);
expect(getDirFileContent(tmpDir, 'instrument.ts')).toEqual(
`import * as Sentry from '@sentry/node';
Sentry.init({
dsn: 'https://example.com' as string,
});`
);
});
});
Loading

0 comments on commit 449bef0

Please sign in to comment.