-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(svelte): Add Component Tracking #5612
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
Changes from 4 commits
0dee42f
e244ff1
cb3def2
81d1213
c2af68f
65e9366
d296d12
ad9b685
3991592
e2e081d
067fc0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'] }, | ||
}), | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// TODO: we might want to call this ui.svelte.init instead because | ||
// it doesn't only track mounting time (there's no before-/afterMount) | ||
// but component init to mount time. | ||
export const UI_SVELTE_MOUNT = 'ui.svelte.mount'; | ||
|
||
export const UI_SVELTE_UPDATE = 'ui.svelte.update'; | ||
|
||
export const DEFAULT_COMPONENT_NAME = 'Svelte Component'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
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_MOUNT, UI_SVELTE_UPDATE } from './constants'; | ||
import { TrackComponentOptions } from './types'; | ||
|
||
const defaultOptions: Required<Pick<TrackComponentOptions, 'trackMount' | 'trackUpdates'>> & | ||
Pick<TrackComponentOptions, 'componentName'> = { | ||
trackMount: 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 = { ...defaultOptions, ...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 mountSpan: Span | undefined = undefined; | ||
if (mergedOptions.trackMount) { | ||
mountSpan = recordMountSpan(transaction, componentName); | ||
} | ||
|
||
if (mergedOptions.trackUpdates) { | ||
recordUpdateSpans(componentName, mountSpan); | ||
} | ||
} | ||
|
||
function recordMountSpan(transaction: Transaction, componentName: string): Span { | ||
const mountSpan = transaction.startChild({ | ||
op: UI_SVELTE_MOUNT, | ||
description: componentName, | ||
}); | ||
|
||
onMount(() => { | ||
mountSpan.finish(); | ||
}); | ||
|
||
return mountSpan; | ||
} | ||
|
||
function recordUpdateSpans(componentName: string, mountSpan?: 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 mounting the component when the update span is started, we start it as child | ||
// of the mount span. Else, we start it as a child of the transaction. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you elaborate a bit why the update span is a child of the component's mount span? Without having thought about it too much it doesn't click for me but maybe there's a reason. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, tbh this is totally up for discussion but the reason why I think it makes sense to make the update a child span of the mount span is because in the component lifecycle of Svelte, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ok. The way you explained this makes a lot of sense. I'd keep it too then! |
||
const parentSpan = | ||
mountSpan && !mountSpan.endTimestamp && mountSpan.transaction === transaction ? mountSpan : 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(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import MagicString from 'magic-string'; | ||
|
||
import { ComponentTrackingInitOptions, PreprocessorGroup, TrackComponentOptions } from './types'; | ||
|
||
const defaultComponentTrackingOptions: Required<ComponentTrackingInitOptions> = { | ||
trackComponents: true, | ||
trackMount: 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 }; | ||
|
||
return { | ||
// 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 }) => { | ||
if (!shouldInjectFunction(mergedOptions.trackComponents, filename)) { | ||
return { code: content }; | ||
} | ||
|
||
const { trackMount, trackUpdates } = mergedOptions; | ||
const trackComponentOptions: TrackComponentOptions = { | ||
Lms24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
trackMount, | ||
trackUpdates, | ||
componentName: getComponentName(filename || ''), | ||
}; | ||
|
||
const importStmt = 'import { trackComponent } from "@sentry/svelte";\n'; | ||
const functionCall = `trackComponent(${JSON.stringify(trackComponentOptions)});\n`; | ||
|
||
const s = new MagicString(content); | ||
s.prepend(functionCall).prepend(importStmt); | ||
|
||
const updatedCode = s.toString(); | ||
const updatedSourceMap = s.generateMap().toString(); | ||
|
||
return { code: updatedCode, map: updatedSourceMap }; | ||
}, | ||
}; | ||
} | ||
|
||
function shouldInjectFunction( | ||
trackComponents: Required<ComponentTrackingInitOptions['trackComponents']>, | ||
filename: string | undefined, | ||
): boolean { | ||
if (!trackComponents || !filename) { | ||
return false; | ||
} | ||
if (Array.isArray(trackComponents)) { | ||
// TODO: this probably needs to be a little more robust | ||
const componentName = getComponentName(filename); | ||
return trackComponents.some(allowed => allowed === componentName); | ||
} | ||
return true; | ||
} | ||
function getComponentName(filename: string): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a node api function that does exactly what this function is doing: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well well well, the more you know 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I tried replacing my function with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO we can just rename |
||
const segments = filename.split('/'); | ||
const componentName = segments[segments.length - 1].replace('.svelte', ''); | ||
return componentName; | ||
Lms24 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// 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>; | ||
|
||
export interface PreprocessorGroup { | ||
markup?: MarkupPreprocessor; | ||
style?: Preprocessor; | ||
script?: Preprocessor; | ||
} | ||
|
||
// 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'; | ||
|
||
export type SpanOptions = { | ||
/** | ||
* If true, spans are be recorded between component intialization and its | ||
* onMount lifecycle hook. | ||
* | ||
* Defaults to true if component tracking is enabled | ||
*/ | ||
trackMount?: boolean; | ||
|
||
/** | ||
* If true, spans are recorded between each component's beforeUpdate and afterUpdate | ||
* lifecycle hooks. | ||
* | ||
* Defaults to true if component tracking is enabled | ||
*/ | ||
trackUpdates?: boolean; | ||
}; | ||
|
||
/** | ||
* Control which components and which operations should be tracked | ||
* and recorded as spans | ||
*/ | ||
export type ComponentTrackingInitOptions = { | ||
/** | ||
* Control if all your Svelte components should be tracked or only a specified list | ||
* of components. | ||
* If set to true, all components will be tracked. | ||
* If you only want to track a selection of components, specify the component names | ||
* as an array. | ||
* | ||
* Defaults to true if the preprocessor is used | ||
*/ | ||
trackComponents?: boolean | string[]; | ||
} & SpanOptions; | ||
|
||
export type TrackComponentOptions = { | ||
componentName?: string; | ||
} & SpanOptions; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24837,6 +24837,11 @@ supports-preserve-symlinks-flag@^1.0.0: | |
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" | ||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== | ||
|
||
[email protected]: | ||
version "3.49.0" | ||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029" | ||
integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA== | ||
|
||
svgo@^1.0.0: | ||
version "1.3.2" | ||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am always in favor of calling things the way users are familiar with. If "initializing" is a term that is used in the svelte community for mounting components, I vote we call it
ui.svelte.init
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My train of thought for maybe renaming this to init instead of mount is that this span does not only track mounting time. Because the
onMount
hook is the only lifecycle hook we have for mounting, we only know when mounting is completed but not when it is started. What we do know, however, is when we start initializing the component (i.e. when the <script> block of a component is executed). So this span tracks exactly this duration: From the beginning of script execution until the component is completely mounted in the DOM. Which technically makes this more an initialization span than a mounting span.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed it to init. Makes more sense IMO. Let's go with that