Skip to content

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

Merged
merged 11 commits into from
Aug 31, 2022
Merged

Conversation

Lms24
Copy link
Member

@Lms24 Lms24 commented Aug 19, 2022

Summary

This PR introduces component tracking to the Sentry Svelte SDK. I'm currently trying to implement an approach for users that is simple and only requires minimal configuration on their end. The idea is to let them specify once if and which components they actually want to track (like we do it in Vue for example).

Because Svelte components are compiled into plain JS at build time, we cannot reliably hook into a Svelte runtime or do similar approaches monkey patching approaches. What we can however do, is to hook into the component compilation by using an official Svelte compiler feature: preprocessors.

With this PR, we add a Svelte preprocessor that injects a function call (+ the necessary import) into the top of the <script> part of Svelte components. The called function takes care creating the lifecycle spans and putting them onto a transaction.

Usage

In your svelte.config.js, import and add the Sentry componentTrackingPreprocessor:

import sveltePreprocess from "svelte-preprocess";
import { componentTrackingPreprocessor } from "@sentry/svelte";

const config = {
  compilerOptions: {
    enableSourcemap: true,
  },
  preprocess: [
    componentTrackingPreprocessor({
      trackComponents: ["Component1", "Component2"],
      trackInit: true,
      trackUpdates: true, 
    }),
    sveltePreprocess({
      sourceMap: true,
    }),
  ],
};

export default config;

Under the Hood

In the compiled JS (after the Svelte compiler is done), this function call translates to the following code for Component1.svelte:

function instance$1($$self, $$props, $$invalidate) {
  let $count;
  component_subscribe($$self, count, ($$value) => $$invalidate(0, $count = $$value));
  // --> here we are:
  trackComponent({
    "trackInit": true,
    "trackUpdates": true,
    "componentName": "Component1"
  });
  // ...
}

Alternative Usage

If you don't want to use our Preprocessor to automatically track components, you can call our tracking function in every component you would like to see tracked yourself:

<script>
  import * as Sentry from "@sentry/svelte";
  Sentry.trackComponent({trackInit: true, trackUpdates: false})
  // rest of your code
</script>

Result

This is the result of the PR:
image

Compatibility with SvelteKit

If users are using the Svelte SDK in a SvelteKit app, they can use this preprocessor (or make the manual function call). I tested component tracking with the Realworld SvelteKit app and it seems to work normally, as long as the SDK is only initialized on the browser end (which is how it is supposed to be used, if at all). There might be weird edge cases but generally, the lifecycle hooks are called on the client-side and the spans are recorded correctly. We can revisit this, if we get bug reports.

To Do

  • revisit types and naming (81d1213)
  • Discuss if code injection approach is ok
  • Check if this is a good approach DevEx-wise
  • Check if/how this works w/ Sveltekit apps
  • Add Unit tests

@Lms24 Lms24 added the Package: svelte Issues related to the Sentry Svelte SDK label Aug 19, 2022
@Lms24 Lms24 added this to the Svelte SDK milestone Aug 19, 2022
@Lms24 Lms24 self-assigned this Aug 19, 2022
Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preprocessor functions should additionally return a map object alongside code and dependencies, where map is a sourcemap representing the transformation.

If this is the case, we might need to use something a library to do this processing, and pass down the emitted sourcemap. For example: https://github.com/sveltejs/svelte-preprocess/blob/main/src/transformers/babel.ts

Comment on lines 32 to 33
options.trackMount && recordMountSpan(transaction, componentName);
options.trackUpdates && recordUpdateSpans(componentName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's make these explicit if statements.

Is there a way we can make the update spans children of the mount spans?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we can make the update spans children of the mount spans?

You mean for the times when update is called directly after onMount?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lms24
Copy link
Member Author

Lms24 commented Aug 29, 2022

If this is the case, we might need to use something a library to do this processing, and pass down the emitted sourcemap. For example: https://github.com/sveltejs/svelte-preprocess/blob/main/src/transformers/babel.ts

Oh good point, thanks for catching that! Makes a lot of sense. I think I'll try out MagicString since we use it already in the Unplugin

@Lms24 Lms24 mentioned this pull request Aug 29, 2022
6 tasks
@github-actions
Copy link
Contributor

github-actions bot commented Aug 29, 2022

size-limit report 📦

Path Size
@sentry/browser - ES5 CDN Bundle (gzipped + minified) 19.42 KB (-0.03% 🔽)
@sentry/browser - ES5 CDN Bundle (minified) 60.09 KB (0%)
@sentry/browser - ES6 CDN Bundle (gzipped + minified) 17.99 KB (-0.01% 🔽)
@sentry/browser - ES6 CDN Bundle (minified) 52.95 KB (0%)
@sentry/browser - Webpack (gzipped + minified) 19.79 KB (0%)
@sentry/browser - Webpack (minified) 64.34 KB (0%)
@sentry/react - Webpack (gzipped + minified) 19.81 KB (0%)
@sentry/nextjs Client - Webpack (gzipped + minified) 44.73 KB (0%)
@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified) 25.91 KB (0%)
@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified) 24.28 KB (-0.01% 🔽)

Copy link
Contributor

@lforst lforst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

}
return true;
}
function getComponentName(filename: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

The 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: path.basename(filename, '.svelte'). No need to change this though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well well well, the more you know 😅

Copy link
Member Author

@Lms24 Lms24 Aug 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I tried replacing my function with path but my Svelte app won't build because I'm using a Node API. There's probably a way to get around this but for now, I'll leave it as is. We can revisit this, if we ever get problems with that function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we can just rename getComponentName to reflect what it's doing (getting the basename) and not worry about using the path API.

Comment on lines 69 to 70
// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 beforeUpdate hook will be called before the mounting of the component is finished. Meaning, the first update of a component is part of its initial initialization. So overall, I think it makes sense to let this update (and only this one) be a child of the mount span.

Copy link
Contributor

Choose a reason for hiding this comment

The 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!

Comment on lines 1 to 4
// 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';
Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Member Author

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

@Lms24 Lms24 requested review from lforst and AbhiPrasad August 31, 2022 10:30
@Lms24 Lms24 marked this pull request as ready for review August 31, 2022 10:30
Copy link
Contributor

@lforst lforst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO nothing major blocking the merging of this. Looking good!

@Lms24 Lms24 merged commit f683059 into master Aug 31, 2022
@Lms24 Lms24 deleted the lms-svelte-component-tracking branch August 31, 2022 11:56
@AbhiPrasad
Copy link
Member

Reminder - we’ll need to add a docs for this feature upon release!

@Lms24
Copy link
Member Author

Lms24 commented Aug 31, 2022

Currently on it ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Package: svelte Issues related to the Sentry Svelte SDK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants