Skip to content

Commit

Permalink
feat(svelte): Add withSentryConfig function to wrap User Svelte Con…
Browse files Browse the repository at this point in the history
…figuration (#5936)

Add a `withSentryConfig` wrapper function to our Svelte SDK which can be used to automatically add Sentry-specific svelte configuration options to our users' configuration in `svelte.config.js`. Going forward, this function will allow us to add more sentry-specific config items, such as additional preprocessors to the config without having to ask our users to adjust their config stuff every time we introduce changes.

The change is fully backward-compatible with how we previously instructed users to add the `componentTrackingPreprocessor` explicitly to their set of preprocessors in their config. However, to make it clear that `withSentryConfig` is the way forward, this PR deprecates `componentTrackingPreprocessor` and in v8, we'll remove it from our public exports.  

Additionally, this patch
- adds tests for `withSentryConfig`.
- removes the svelte-internal types we previously copied from Svelte source code into our types file
  • Loading branch information
Lms24 committed Oct 12, 2022
1 parent 2a95451 commit 952360e
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 38 deletions.
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';

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

0 comments on commit 952360e

Please sign in to comment.