Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(svelte): Add withSentryConfig function to wrap User Svelte Configuration #5936

Merged
merged 8 commits into from Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/svelte/src/config.ts
@@ -0,0 +1,73 @@
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';

import { componentTrackingPreprocessor, defaultComponentTrackingOptions } from './preprocessors';
import { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from './types';

const DEFAULT_SENTRY_OPTIONS: SentrySvelteConfigOptions = {
componentTracking: defaultComponentTrackingOptions,
};

/**
* Add Sentry options to the Svelte config to be exported from the user's `svelte.config.js` file.
*
* @param originalConfig The existing config to be exported prior to adding Sentry
* @param sentryOptions The configuration of the Sentry-added options
*
* @return The wrapped and modified config to be exported
*/
export function withSentryConfig(
originalConfig: SvelteConfig,
sentryOptions?: SentrySvelteConfigOptions,
): SvelteConfig {
const mergedOptions = {
...DEFAULT_SENTRY_OPTIONS,
...sentryOptions,
};

const originalPreprocessors = getOriginalPreprocessorArray(originalConfig);

// Map is insertion-order-preserving. It's important to add preprocessors
// to this map in the right order we want to see them being executed.
// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
const sentryPreprocessors = new Map<string, SentryPreprocessorGroup>();

const shouldTrackComponents = mergedOptions.componentTracking && mergedOptions.componentTracking.trackComponents;
if (shouldTrackComponents) {
// TODO(v8): Remove eslint rule
// eslint-disable-next-line deprecation/deprecation
const firstPassPreproc: SentryPreprocessorGroup = componentTrackingPreprocessor(mergedOptions.componentTracking);
sentryPreprocessors.set(firstPassPreproc.sentryId || '', firstPassPreproc);
}

// We prioritize user-added preprocessors, so we don't insert sentry processors if they
// have already been added by users.
originalPreprocessors.forEach((p: SentryPreprocessorGroup) => {
if (p.sentryId) {
sentryPreprocessors.delete(p.sentryId);
}
});

const mergedPreprocessors = [...sentryPreprocessors.values(), ...originalPreprocessors];

return {
...originalConfig,
preprocess: mergedPreprocessors,
};
}

/**
* Standardizes the different ways the user-provided preprocessor option can be specified.
* Users can specify an array of preprocessors, a single one or no preprocessor.
*
* @param originalConfig the user-provided svelte config oject
* @return an array of preprocessors or an empty array if no preprocessors were specified
*/
function getOriginalPreprocessorArray(originalConfig: SvelteConfig): PreprocessorGroup[] {
if (originalConfig.preprocess) {
if (Array.isArray(originalConfig.preprocess)) {
return originalConfig.preprocess;
}
return [originalConfig.preprocess];
}
return [];
}
5 changes: 5 additions & 0 deletions packages/svelte/src/index.ts
Expand Up @@ -7,5 +7,10 @@ export * from '@sentry/browser';

export { init } from './sdk';

// TODO(v8): Remove this export
// eslint-disable-next-line deprecation/deprecation
export { componentTrackingPreprocessor } from './preprocessors';

export { trackComponent } from './performance';

export { withSentryConfig } from './config';
20 changes: 16 additions & 4 deletions packages/svelte/src/preprocessors.ts
@@ -1,25 +1,30 @@
import MagicString from 'magic-string';
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';

import { ComponentTrackingInitOptions, PreprocessorGroup, TrackComponentOptions } from './types';
import { ComponentTrackingInitOptions, SentryPreprocessorGroup, TrackComponentOptions } from './types';

export const defaultComponentTrackingOptions: Required<ComponentTrackingInitOptions> = {
trackComponents: true,
trackInit: true,
trackUpdates: true,
};

export const FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID = 'FIRST_PASS_COMPONENT_TRACKING_PREPROCESSOR';

/**
* Svelte Preprocessor to inject Sentry performance monitoring related code
* into Svelte components.
*
* @deprecated Use `withSentryConfig` which is the new way of making compile-time modifications
* to Svelte apps going forward.
*/
export function componentTrackingPreprocessor(options?: ComponentTrackingInitOptions): PreprocessorGroup {
const mergedOptions = { ...defaultComponentTrackingOptions, ...options };

const visitedFiles = new Set<string>();

return {
// This script hook is called whenever a Svelte component's <script>
// content is preprocessed.
const preprocessor: PreprocessorGroup = {
// This script hook is called whenever a Svelte component's <script> content is preprocessed.
// `content` contains the script code as a string
script: ({ content, filename, attributes }) => {
// TODO: Not sure when a filename could be undefined. Using this 'unknown' fallback for the time being
Expand Down Expand Up @@ -48,6 +53,13 @@ export function componentTrackingPreprocessor(options?: ComponentTrackingInitOpt
return { code: updatedCode, map: updatedSourceMap };
},
};

const sentryPreprocessor: SentryPreprocessorGroup = {
...preprocessor,
sentryId: FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID,
};

return sentryPreprocessor;
}

function shouldInjectFunction(
Expand Down
55 changes: 21 additions & 34 deletions packages/svelte/src/types.ts
@@ -1,40 +1,27 @@
// The following types were copied from 'svelte/compiler'-internal
// type definitions
// see: https://github.com/sveltejs/svelte/blob/master/src/compiler/preprocess/types.ts
interface Processed {
code: string;
map?: string | Record<string, unknown>;
dependencies?: string[];
toString?: () => string;
}

type MarkupPreprocessor = (options: {
content: string;
filename?: string;
}) => Processed | void | Promise<Processed | void>;

type Preprocessor = (options: {
/**
* The script/style tag content
*/
content: string;
attributes: Record<string, string | boolean>;
/**
* The whole Svelte file content
*/
markup: string;
filename?: string;
}) => Processed | void | Promise<Processed | void>;
import { CompileOptions } from 'svelte/types/compiler';
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
Lms24 marked this conversation as resolved.
Show resolved Hide resolved

export interface PreprocessorGroup {
markup?: MarkupPreprocessor;
style?: Preprocessor;
script?: Preprocessor;
// Adds an id property to the preprocessor object we can use to check for duplication
// in the preprocessors array
export interface SentryPreprocessorGroup extends PreprocessorGroup {
sentryId?: string;
}

// Alternatively, we could use a direct from svelte/compiler/preprocess
// TODO: figure out what's better and roll with that
// import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
/**
* The object exported from `svelte.config.js`
*/
export type SvelteConfig = {
[key: string]: unknown;
preprocess?: PreprocessorGroup[] | PreprocessorGroup;
compilerOptions?: CompileOptions;
};

/**
* Options users can provide to `withSentryConfig` to customize what Sentry adds too the Svelte config
*/
export type SentrySvelteConfigOptions = {
componentTracking?: ComponentTrackingInitOptions;
};

export type SpanOptions = {
/**
Expand Down
91 changes: 91 additions & 0 deletions packages/svelte/test/config.test.ts
@@ -0,0 +1,91 @@
import { withSentryConfig } from '../src/config';
import { componentTrackingPreprocessor, FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID } from '../src/preprocessors';
import { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from '../src/types';

describe('withSentryConfig', () => {
it.each([
[
'no preprocessors specified',
{
compilerOptions: {
enableSourcemap: true,
},
},
],
[
'a single preprocessor specified',
{
compilerOptions: {
enableSourcemap: true,
},
preprocess: {},
},
],
[
'an array of preprocessors specified',
{
compilerOptions: {
enableSourcemap: true,
},
preprocess: [{}, {}, {}],
},
],
])('adds our preprocessors by default to the provided svelte config with %s', (_, originalConfig: SvelteConfig) => {
const wrappedConfig = withSentryConfig(originalConfig);
const originalPreprocs = originalConfig.preprocess;
const originalNumberOfPreprocs = originalPreprocs
? Array.isArray(originalPreprocs)
? originalPreprocs.length
: 1
: 0;

expect(Array.isArray(wrappedConfig.preprocess)).toBe(true);
expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: expect.any(Array) });
expect(wrappedConfig.preprocess).toHaveLength(originalNumberOfPreprocs + 1);
expect((wrappedConfig.preprocess as SentryPreprocessorGroup[])[0].sentryId).toEqual(
FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID,
);
});

it("doesn't add Sentry preprocessors that were already added by the users", () => {
// eslint-disable-next-line deprecation/deprecation
const sentryPreproc = componentTrackingPreprocessor();
const originalConfig = {
compilerOptions: {
enableSourcemap: true,
},
preprocess: sentryPreproc,
};

const wrappedConfig = withSentryConfig(originalConfig);

expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: [sentryPreproc] });
});

it('handles multiple wraps correctly by only adding our preprocessors once', () => {
const originalConfig = {
compilerOptions: {
enableSourcemap: true,
},
};

const wrappedConfig = withSentryConfig(withSentryConfig(withSentryConfig(originalConfig)));

expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: expect.any(Array) });
expect(wrappedConfig.preprocess).toHaveLength(1);
});

it("doesn't add component tracking preprocessors if the feature is deactivated", () => {
const originalConfig = {
compilerOptions: {
enableSourcemap: true,
},
preprocess: [{}],
};

const sentryOptions: SentrySvelteConfigOptions = { componentTracking: { trackComponents: false } };
const wrappedConfig = withSentryConfig(originalConfig, sentryOptions);

expect(wrappedConfig).toEqual(originalConfig);
});
});
1 change: 1 addition & 0 deletions packages/svelte/test/preprocessors.test.ts
@@ -1,3 +1,4 @@
/* eslint-disable deprecation/deprecation */
import { componentTrackingPreprocessor, defaultComponentTrackingOptions } from '../src/preprocessors';

function expectComponentCodeToBeModified(
Expand Down