diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 59d9ae81ea6e..279e6bfeeb14 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -19,11 +19,15 @@ "@sentry/browser": "7.11.1", "@sentry/types": "7.11.1", "@sentry/utils": "7.11.1", + "magic-string": "^0.26.2", "tslib": "^1.9.3" }, "peerDependencies": { "svelte": "3.x" }, + "devDependencies": { + "svelte": "3.49.0" + }, "scripts": { "build": "run-p build:rollup build:types", "build:dev": "run-s build", diff --git a/packages/svelte/rollup.npm.config.js b/packages/svelte/rollup.npm.config.js index 5a62b528ef44..ae3c7a9a5b8a 100644 --- a/packages/svelte/rollup.npm.config.js +++ b/packages/svelte/rollup.npm.config.js @@ -1,3 +1,8 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; -export default makeNPMConfigVariants(makeBaseNPMConfig()); +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + // Prevent 'svelte/internal' stuff from being included in the built JS + packageSpecificConfig: { external: ['svelte/internal'] }, + }), +); diff --git a/packages/svelte/src/constants.ts b/packages/svelte/src/constants.ts new file mode 100644 index 000000000000..cb8255040c03 --- /dev/null +++ b/packages/svelte/src/constants.ts @@ -0,0 +1,5 @@ +export const UI_SVELTE_INIT = 'ui.svelte.init'; + +export const UI_SVELTE_UPDATE = 'ui.svelte.update'; + +export const DEFAULT_COMPONENT_NAME = 'Svelte Component'; diff --git a/packages/svelte/src/index.ts b/packages/svelte/src/index.ts index 8e25b84c4a0c..40f648a46286 100644 --- a/packages/svelte/src/index.ts +++ b/packages/svelte/src/index.ts @@ -1,3 +1,11 @@ +export type { + ComponentTrackingInitOptions as ComponentTrackingOptions, + TrackComponentOptions as TrackingOptions, +} from './types'; + export * from '@sentry/browser'; export { init } from './sdk'; + +export { componentTrackingPreprocessor } from './preprocessors'; +export { trackComponent } from './performance'; diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts new file mode 100644 index 000000000000..359950e41264 --- /dev/null +++ b/packages/svelte/src/performance.ts @@ -0,0 +1,96 @@ +import { getCurrentHub } from '@sentry/browser'; +import { Span, Transaction } from '@sentry/types'; +import { afterUpdate, beforeUpdate, onMount } from 'svelte'; +import { current_component } from 'svelte/internal'; + +import { DEFAULT_COMPONENT_NAME, UI_SVELTE_INIT, UI_SVELTE_UPDATE } from './constants'; +import { TrackComponentOptions } from './types'; + +const defaultTrackComponentOptions: { + trackInit: boolean; + trackUpdates: boolean; + componentName?: string; +} = { + trackInit: true, + trackUpdates: true, +}; + +/** + * Tracks the Svelte component's intialization and mounting operation as well as + * updates and records them as spans. + * This function is injected automatically into your Svelte components' code + * if you are using the Sentry componentTrackingPreprocessor. + * Alternatively, you can call it yourself if you don't want to use the preprocessor. + */ +export function trackComponent(options?: TrackComponentOptions): void { + const mergedOptions = { ...defaultTrackComponentOptions, ...options }; + + const transaction = getActiveTransaction(); + if (!transaction) { + return; + } + + const customComponentName = mergedOptions.componentName; + + // current_component.ctor.name is likely to give us the component's name automatically + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const componentName = `<${customComponentName || current_component.constructor.name || DEFAULT_COMPONENT_NAME}>`; + + let initSpan: Span | undefined = undefined; + if (mergedOptions.trackInit) { + initSpan = recordInitSpan(transaction, componentName); + } + + if (mergedOptions.trackUpdates) { + recordUpdateSpans(componentName, initSpan); + } +} + +function recordInitSpan(transaction: Transaction, componentName: string): Span { + const initSpan = transaction.startChild({ + op: UI_SVELTE_INIT, + description: componentName, + }); + + onMount(() => { + initSpan.finish(); + }); + + return initSpan; +} + +function recordUpdateSpans(componentName: string, initSpan?: Span): void { + let updateSpan: Span | undefined; + beforeUpdate(() => { + // We need to get the active transaction again because the initial one could + // already be finished or there is currently no transaction going on. + const transaction = getActiveTransaction(); + if (!transaction) { + return; + } + + // If we are initializing the component when the update span is started, we start it as child + // of the init span. Else, we start it as a child of the transaction. + const parentSpan = + initSpan && !initSpan.endTimestamp && initSpan.transaction === transaction ? initSpan : transaction; + + updateSpan = parentSpan.startChild({ + op: UI_SVELTE_UPDATE, + description: componentName, + }); + }); + + afterUpdate(() => { + if (!updateSpan) { + return; + } + updateSpan.finish(); + updateSpan = undefined; + }); +} + +function getActiveTransaction(): Transaction | undefined { + const currentHub = getCurrentHub(); + const scope = currentHub && currentHub.getScope(); + return scope && scope.getTransaction(); +} diff --git a/packages/svelte/src/preprocessors.ts b/packages/svelte/src/preprocessors.ts new file mode 100644 index 000000000000..9102636c045f --- /dev/null +++ b/packages/svelte/src/preprocessors.ts @@ -0,0 +1,89 @@ +import MagicString from 'magic-string'; + +import { ComponentTrackingInitOptions, PreprocessorGroup, TrackComponentOptions } from './types'; + +export const defaultComponentTrackingOptions: Required = { + trackComponents: true, + trackInit: true, + trackUpdates: true, +}; + +/** + * Svelte Preprocessor to inject Sentry performance monitoring related code + * into Svelte components. + */ +export function componentTrackingPreprocessor(options?: ComponentTrackingInitOptions): PreprocessorGroup { + const mergedOptions = { ...defaultComponentTrackingOptions, ...options }; + + const visitedFiles = new Set(); + + return { + // This script hook is called whenever a Svelte component's