Skip to content

Commit 952360e

Browse files
authored
feat(svelte): Add withSentryConfig function to wrap User Svelte Configuration (#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
1 parent 2a95451 commit 952360e

File tree

6 files changed

+207
-38
lines changed

6 files changed

+207
-38
lines changed

packages/svelte/src/config.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
2+
3+
import { componentTrackingPreprocessor, defaultComponentTrackingOptions } from './preprocessors';
4+
import { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from './types';
5+
6+
const DEFAULT_SENTRY_OPTIONS: SentrySvelteConfigOptions = {
7+
componentTracking: defaultComponentTrackingOptions,
8+
};
9+
10+
/**
11+
* Add Sentry options to the Svelte config to be exported from the user's `svelte.config.js` file.
12+
*
13+
* @param originalConfig The existing config to be exported prior to adding Sentry
14+
* @param sentryOptions The configuration of the Sentry-added options
15+
*
16+
* @return The wrapped and modified config to be exported
17+
*/
18+
export function withSentryConfig(
19+
originalConfig: SvelteConfig,
20+
sentryOptions?: SentrySvelteConfigOptions,
21+
): SvelteConfig {
22+
const mergedOptions = {
23+
...DEFAULT_SENTRY_OPTIONS,
24+
...sentryOptions,
25+
};
26+
27+
const originalPreprocessors = getOriginalPreprocessorArray(originalConfig);
28+
29+
// Map is insertion-order-preserving. It's important to add preprocessors
30+
// to this map in the right order we want to see them being executed.
31+
// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
32+
const sentryPreprocessors = new Map<string, SentryPreprocessorGroup>();
33+
34+
const shouldTrackComponents = mergedOptions.componentTracking && mergedOptions.componentTracking.trackComponents;
35+
if (shouldTrackComponents) {
36+
// TODO(v8): Remove eslint rule
37+
// eslint-disable-next-line deprecation/deprecation
38+
const firstPassPreproc: SentryPreprocessorGroup = componentTrackingPreprocessor(mergedOptions.componentTracking);
39+
sentryPreprocessors.set(firstPassPreproc.sentryId || '', firstPassPreproc);
40+
}
41+
42+
// We prioritize user-added preprocessors, so we don't insert sentry processors if they
43+
// have already been added by users.
44+
originalPreprocessors.forEach((p: SentryPreprocessorGroup) => {
45+
if (p.sentryId) {
46+
sentryPreprocessors.delete(p.sentryId);
47+
}
48+
});
49+
50+
const mergedPreprocessors = [...sentryPreprocessors.values(), ...originalPreprocessors];
51+
52+
return {
53+
...originalConfig,
54+
preprocess: mergedPreprocessors,
55+
};
56+
}
57+
58+
/**
59+
* Standardizes the different ways the user-provided preprocessor option can be specified.
60+
* Users can specify an array of preprocessors, a single one or no preprocessor.
61+
*
62+
* @param originalConfig the user-provided svelte config oject
63+
* @return an array of preprocessors or an empty array if no preprocessors were specified
64+
*/
65+
function getOriginalPreprocessorArray(originalConfig: SvelteConfig): PreprocessorGroup[] {
66+
if (originalConfig.preprocess) {
67+
if (Array.isArray(originalConfig.preprocess)) {
68+
return originalConfig.preprocess;
69+
}
70+
return [originalConfig.preprocess];
71+
}
72+
return [];
73+
}

packages/svelte/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,10 @@ export * from '@sentry/browser';
77

88
export { init } from './sdk';
99

10+
// TODO(v8): Remove this export
11+
// eslint-disable-next-line deprecation/deprecation
1012
export { componentTrackingPreprocessor } from './preprocessors';
13+
1114
export { trackComponent } from './performance';
15+
16+
export { withSentryConfig } from './config';

packages/svelte/src/preprocessors.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
import MagicString from 'magic-string';
2+
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
23

3-
import { ComponentTrackingInitOptions, PreprocessorGroup, TrackComponentOptions } from './types';
4+
import { ComponentTrackingInitOptions, SentryPreprocessorGroup, TrackComponentOptions } from './types';
45

56
export const defaultComponentTrackingOptions: Required<ComponentTrackingInitOptions> = {
67
trackComponents: true,
78
trackInit: true,
89
trackUpdates: true,
910
};
1011

12+
export const FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID = 'FIRST_PASS_COMPONENT_TRACKING_PREPROCESSOR';
13+
1114
/**
1215
* Svelte Preprocessor to inject Sentry performance monitoring related code
1316
* into Svelte components.
17+
*
18+
* @deprecated Use `withSentryConfig` which is the new way of making compile-time modifications
19+
* to Svelte apps going forward.
1420
*/
1521
export function componentTrackingPreprocessor(options?: ComponentTrackingInitOptions): PreprocessorGroup {
1622
const mergedOptions = { ...defaultComponentTrackingOptions, ...options };
1723

1824
const visitedFiles = new Set<string>();
1925

20-
return {
21-
// This script hook is called whenever a Svelte component's <script>
22-
// content is preprocessed.
26+
const preprocessor: PreprocessorGroup = {
27+
// This script hook is called whenever a Svelte component's <script> content is preprocessed.
2328
// `content` contains the script code as a string
2429
script: ({ content, filename, attributes }) => {
2530
// TODO: Not sure when a filename could be undefined. Using this 'unknown' fallback for the time being
@@ -48,6 +53,13 @@ export function componentTrackingPreprocessor(options?: ComponentTrackingInitOpt
4853
return { code: updatedCode, map: updatedSourceMap };
4954
},
5055
};
56+
57+
const sentryPreprocessor: SentryPreprocessorGroup = {
58+
...preprocessor,
59+
sentryId: FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID,
60+
};
61+
62+
return sentryPreprocessor;
5163
}
5264

5365
function shouldInjectFunction(

packages/svelte/src/types.ts

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
1-
// The following types were copied from 'svelte/compiler'-internal
2-
// type definitions
3-
// see: https://github.com/sveltejs/svelte/blob/master/src/compiler/preprocess/types.ts
4-
interface Processed {
5-
code: string;
6-
map?: string | Record<string, unknown>;
7-
dependencies?: string[];
8-
toString?: () => string;
9-
}
10-
11-
type MarkupPreprocessor = (options: {
12-
content: string;
13-
filename?: string;
14-
}) => Processed | void | Promise<Processed | void>;
15-
16-
type Preprocessor = (options: {
17-
/**
18-
* The script/style tag content
19-
*/
20-
content: string;
21-
attributes: Record<string, string | boolean>;
22-
/**
23-
* The whole Svelte file content
24-
*/
25-
markup: string;
26-
filename?: string;
27-
}) => Processed | void | Promise<Processed | void>;
1+
import { CompileOptions } from 'svelte/types/compiler';
2+
import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
283

29-
export interface PreprocessorGroup {
30-
markup?: MarkupPreprocessor;
31-
style?: Preprocessor;
32-
script?: Preprocessor;
4+
// Adds an id property to the preprocessor object we can use to check for duplication
5+
// in the preprocessors array
6+
export interface SentryPreprocessorGroup extends PreprocessorGroup {
7+
sentryId?: string;
338
}
349

35-
// Alternatively, we could use a direct from svelte/compiler/preprocess
36-
// TODO: figure out what's better and roll with that
37-
// import { PreprocessorGroup } from 'svelte/types/compiler/preprocess';
10+
/**
11+
* The object exported from `svelte.config.js`
12+
*/
13+
export type SvelteConfig = {
14+
[key: string]: unknown;
15+
preprocess?: PreprocessorGroup[] | PreprocessorGroup;
16+
compilerOptions?: CompileOptions;
17+
};
18+
19+
/**
20+
* Options users can provide to `withSentryConfig` to customize what Sentry adds too the Svelte config
21+
*/
22+
export type SentrySvelteConfigOptions = {
23+
componentTracking?: ComponentTrackingInitOptions;
24+
};
3825

3926
export type SpanOptions = {
4027
/**

packages/svelte/test/config.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { withSentryConfig } from '../src/config';
2+
import { componentTrackingPreprocessor, FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID } from '../src/preprocessors';
3+
import { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from '../src/types';
4+
5+
describe('withSentryConfig', () => {
6+
it.each([
7+
[
8+
'no preprocessors specified',
9+
{
10+
compilerOptions: {
11+
enableSourcemap: true,
12+
},
13+
},
14+
],
15+
[
16+
'a single preprocessor specified',
17+
{
18+
compilerOptions: {
19+
enableSourcemap: true,
20+
},
21+
preprocess: {},
22+
},
23+
],
24+
[
25+
'an array of preprocessors specified',
26+
{
27+
compilerOptions: {
28+
enableSourcemap: true,
29+
},
30+
preprocess: [{}, {}, {}],
31+
},
32+
],
33+
])('adds our preprocessors by default to the provided svelte config with %s', (_, originalConfig: SvelteConfig) => {
34+
const wrappedConfig = withSentryConfig(originalConfig);
35+
const originalPreprocs = originalConfig.preprocess;
36+
const originalNumberOfPreprocs = originalPreprocs
37+
? Array.isArray(originalPreprocs)
38+
? originalPreprocs.length
39+
: 1
40+
: 0;
41+
42+
expect(Array.isArray(wrappedConfig.preprocess)).toBe(true);
43+
expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: expect.any(Array) });
44+
expect(wrappedConfig.preprocess).toHaveLength(originalNumberOfPreprocs + 1);
45+
expect((wrappedConfig.preprocess as SentryPreprocessorGroup[])[0].sentryId).toEqual(
46+
FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID,
47+
);
48+
});
49+
50+
it("doesn't add Sentry preprocessors that were already added by the users", () => {
51+
// eslint-disable-next-line deprecation/deprecation
52+
const sentryPreproc = componentTrackingPreprocessor();
53+
const originalConfig = {
54+
compilerOptions: {
55+
enableSourcemap: true,
56+
},
57+
preprocess: sentryPreproc,
58+
};
59+
60+
const wrappedConfig = withSentryConfig(originalConfig);
61+
62+
expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: [sentryPreproc] });
63+
});
64+
65+
it('handles multiple wraps correctly by only adding our preprocessors once', () => {
66+
const originalConfig = {
67+
compilerOptions: {
68+
enableSourcemap: true,
69+
},
70+
};
71+
72+
const wrappedConfig = withSentryConfig(withSentryConfig(withSentryConfig(originalConfig)));
73+
74+
expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: expect.any(Array) });
75+
expect(wrappedConfig.preprocess).toHaveLength(1);
76+
});
77+
78+
it("doesn't add component tracking preprocessors if the feature is deactivated", () => {
79+
const originalConfig = {
80+
compilerOptions: {
81+
enableSourcemap: true,
82+
},
83+
preprocess: [{}],
84+
};
85+
86+
const sentryOptions: SentrySvelteConfigOptions = { componentTracking: { trackComponents: false } };
87+
const wrappedConfig = withSentryConfig(originalConfig, sentryOptions);
88+
89+
expect(wrappedConfig).toEqual(originalConfig);
90+
});
91+
});

packages/svelte/test/preprocessors.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable deprecation/deprecation */
12
import { componentTrackingPreprocessor, defaultComponentTrackingOptions } from '../src/preprocessors';
23

34
function expectComponentCodeToBeModified(

0 commit comments

Comments
 (0)