@greenhat-services/magpie-vue 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Magpie contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # @greenhat-services/magpie-vue
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Magpie error tracking and performance monitoring for Vue 3 and Vue Router.
4
+
5
+ ```sh
6
+ npm install @greenhat-services/magpie-vue
7
+ ```
8
+
9
+ Reports Vue errors with the component they came from (never its props), and names page loads and navigations by route pattern.
10
+
11
+ ```ts
12
+ Magpie.init({ app, router, dsn: import.meta.env.VITE_MAGPIE_DSN, release: import.meta.env.VITE_MAGPIE_RELEASE, tracesSampleRate: 0.2 });
13
+ ```
14
+
15
+ Pair it with `@greenhat-services/magpie-vite-plugin` for readable stack traces.
16
+
17
+ Full documentation, options and what is (and is not) sent: https://github.com/Greenhat-Services/magpie-js#readme
18
+
19
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ var magpieBrowser = require('@greenhat-services/magpie-browser');
4
+
5
+ // src/index.ts
6
+ function componentHierarchy(instance) {
7
+ const names = [];
8
+ for (let node = instance, depth = 0; node && depth < 20; node = node.$parent, depth++) {
9
+ names.push(componentName(node));
10
+ }
11
+ return names;
12
+ }
13
+ function componentName(instance) {
14
+ const options = instance?.$options ?? {};
15
+ const file = options.__file?.split(/[\\/]/).pop()?.replace(/\.vue$/, "");
16
+ return options.name || options.__name || file || "Anonymous";
17
+ }
18
+ var VUE_ERROR_SITES = {
19
+ sp: "serverPrefetch hook",
20
+ bc: "beforeCreate hook",
21
+ c: "created hook",
22
+ bm: "beforeMount hook",
23
+ m: "mounted hook",
24
+ bu: "beforeUpdate hook",
25
+ u: "updated hook",
26
+ bum: "beforeUnmount hook",
27
+ um: "unmounted hook",
28
+ a: "activated hook",
29
+ da: "deactivated hook",
30
+ ec: "errorCaptured hook",
31
+ rtc: "renderTracked hook",
32
+ rtg: "renderTriggered hook",
33
+ "0": "setup function",
34
+ "1": "render function",
35
+ "2": "watcher getter",
36
+ "3": "watcher callback",
37
+ "4": "watcher cleanup function",
38
+ "5": "native event handler",
39
+ "6": "component event handler",
40
+ "7": "vnode hook",
41
+ "8": "directive hook",
42
+ "9": "transition hook",
43
+ "10": "app errorHandler",
44
+ "11": "app warnHandler",
45
+ "12": "ref function",
46
+ "13": "async component loader",
47
+ "14": "scheduler flush",
48
+ "15": "component update",
49
+ "16": "app unmount cleanup function"
50
+ };
51
+ function describeErrorSite(info) {
52
+ const code = /#runtime-([a-z0-9]+)$/i.exec(info)?.[1];
53
+ return code && VUE_ERROR_SITES[code] || info;
54
+ }
55
+ function captureVueError(error, instance, info) {
56
+ const client = magpieBrowser.getClient();
57
+ if (!client) return;
58
+ client.withScope((scope) => {
59
+ const file = instance?.$options?.__file;
60
+ scope.setContext("vue", {
61
+ componentName: componentName(instance),
62
+ lifecycleHook: describeErrorSite(info),
63
+ hierarchy: componentHierarchy(instance).join(" > "),
64
+ ...file ? { file: file.split(/[\\/]/).slice(-3).join("/") } : {}
65
+ });
66
+ scope.setTag("vue.component", componentName(instance));
67
+ magpieBrowser.captureException(error, { mechanism: { type: "vue", handled: false } });
68
+ });
69
+ }
70
+ function attachErrorHandler(app) {
71
+ const previous = app.config.errorHandler;
72
+ app.config.errorHandler = (error, instance, info) => {
73
+ try {
74
+ captureVueError(error, instance, info);
75
+ } catch {
76
+ }
77
+ if (previous) previous.call(app, error, instance, info);
78
+ else console.error(error);
79
+ };
80
+ }
81
+ function routeName(route) {
82
+ const pattern = route.matched[route.matched.length - 1]?.path?.replace(/\(\)/g, "");
83
+ return pattern || (typeof route.name === "string" ? route.name : "") || route.path;
84
+ }
85
+ function routerTracing(router) {
86
+ return {
87
+ name: "VueRouter",
88
+ setup(client) {
89
+ let initial = true;
90
+ const removeBefore = router.beforeEach((to) => {
91
+ const name = routeName(to);
92
+ if (initial) magpieBrowser.renameActiveTransaction(client, name);
93
+ else magpieBrowser.startNavigation(client, name);
94
+ client.scope.setTransactionName(name);
95
+ return void 0;
96
+ });
97
+ const removeAfter = router.afterEach(() => {
98
+ initial = false;
99
+ });
100
+ const removeError = router.onError((error) => {
101
+ magpieBrowser.captureException(error, { mechanism: { type: "vue-router", handled: true } });
102
+ });
103
+ return () => {
104
+ removeBefore();
105
+ removeAfter();
106
+ removeError();
107
+ };
108
+ }
109
+ };
110
+ }
111
+ var VUE_SDK_NAME = "magpie.javascript.vue";
112
+ function init(options = {}) {
113
+ const { app, router, integrations = [], ...rest } = options;
114
+ const client = magpieBrowser.init({
115
+ ...rest,
116
+ sdkName: rest.sdkName ?? VUE_SDK_NAME,
117
+ integrations: [...router ? [routerTracing(router)] : [], ...integrations]
118
+ });
119
+ if (client && app) attachErrorHandler(app);
120
+ return client;
121
+ }
122
+
123
+ exports.VUE_SDK_NAME = VUE_SDK_NAME;
124
+ exports.attachErrorHandler = attachErrorHandler;
125
+ exports.captureVueError = captureVueError;
126
+ exports.componentHierarchy = componentHierarchy;
127
+ exports.componentName = componentName;
128
+ exports.describeErrorSite = describeErrorSite;
129
+ exports.init = init;
130
+ exports.routeName = routeName;
131
+ exports.routerTracing = routerTracing;
132
+ Object.keys(magpieBrowser).forEach(function (k) {
133
+ if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
134
+ enumerable: true,
135
+ get: function () { return magpieBrowser[k]; }
136
+ });
137
+ });
138
+ //# sourceMappingURL=index.cjs.map
139
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errorHandler.ts","../src/router.ts","../src/index.ts"],"names":["getClient","captureException","renameActiveTransaction","startNavigation","browserInit"],"mappings":"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","file":"index.cjs","sourcesContent":["import { captureException, getClient } from '@greenhat-services/magpie-browser';\nimport type { App, ComponentPublicInstance } from 'vue';\n\n/**\n * Where in the component tree a Vue error happened, in words: `UserCard > UserList > App`. Names only, no props or\n * state, which can hold personal data. Capped so a deep tree stays small.\n */\nexport function componentHierarchy(instance: ComponentPublicInstance | null | undefined): string[] {\n const names: string[] = [];\n\n for (let node: ComponentPublicInstance | null | undefined = instance, depth = 0; node && depth < 20; node = node.$parent, depth++) {\n names.push(componentName(node));\n }\n\n return names;\n}\n\nexport function componentName(instance: ComponentPublicInstance | null | undefined): string {\n const options = (instance?.$options ?? {}) as { name?: string; __name?: string; __file?: string };\n const file = options.__file?.split(/[\\\\/]/).pop()?.replace(/\\.vue$/, '');\n\n return options.name || options.__name || file || 'Anonymous';\n}\n\n/**\n * Vue's names for where an error happened. Development builds pass these as words; production builds pass a link\n * (`https://vuejs.org/error-reference/#runtime-5`) to save bytes, so the code is turned back into the words here.\n * Numbering is Vue 3.5's `ErrorCodes`; an unknown code is kept as given.\n */\nconst VUE_ERROR_SITES: Record<string, string> = {\n sp: 'serverPrefetch hook', bc: 'beforeCreate hook', c: 'created hook', bm: 'beforeMount hook', m: 'mounted hook',\n bu: 'beforeUpdate hook', u: 'updated hook', bum: 'beforeUnmount hook', um: 'unmounted hook', a: 'activated hook',\n da: 'deactivated hook', ec: 'errorCaptured hook', rtc: 'renderTracked hook', rtg: 'renderTriggered hook',\n '0': 'setup function', '1': 'render function', '2': 'watcher getter', '3': 'watcher callback', '4': 'watcher cleanup function',\n '5': 'native event handler', '6': 'component event handler', '7': 'vnode hook', '8': 'directive hook', '9': 'transition hook',\n '10': 'app errorHandler', '11': 'app warnHandler', '12': 'ref function', '13': 'async component loader', '14': 'scheduler flush',\n '15': 'component update', '16': 'app unmount cleanup function',\n};\n\nexport function describeErrorSite(info: string): string {\n const code = /#runtime-([a-z0-9]+)$/i.exec(info)?.[1];\n\n return (code && VUE_ERROR_SITES[code]) || info;\n}\n\n/** Reports an error Vue caught, with the component it came from and the lifecycle step it was in (\"setup function\", \"event handler\"). */\nexport function captureVueError(error: unknown, instance: ComponentPublicInstance | null | undefined, info: string): void {\n const client = getClient();\n if (!client) return;\n\n client.withScope((scope) => {\n const file = (instance?.$options as { __file?: string } | undefined)?.__file;\n scope.setContext('vue', {\n componentName: componentName(instance),\n lifecycleHook: describeErrorSite(info),\n hierarchy: componentHierarchy(instance).join(' > '),\n ...(file ? { file: file.split(/[\\\\/]/).slice(-3).join('/') } : {}),\n });\n scope.setTag('vue.component', componentName(instance));\n\n captureException(error, { mechanism: { type: 'vue', handled: false } });\n });\n}\n\n/**\n * Sets `app.config.errorHandler`, keeping whatever handler the app already had. Vue stops logging errors to the\n * console as soon as a handler exists, so when there was none the error is logged here: Magpie never hides an error\n * from the developer who is looking at the console.\n */\nexport function attachErrorHandler(app: App): void {\n const previous = app.config.errorHandler;\n\n app.config.errorHandler = (error, instance, info) => {\n try {\n captureVueError(error, instance, info);\n } catch {\n // Reporting must never be the thing that breaks the app.\n }\n\n if (previous) previous.call(app, error, instance, info);\n else console.error(error);\n };\n}\n","import { captureException, type Client, type Integration, renameActiveTransaction, startNavigation } from '@greenhat-services/magpie-browser';\n\n/** The parts of a Vue Router route this reads. Structural, so the SDK needs no import from `vue-router`. */\nexport interface RouteLike {\n path: string;\n name?: string | symbol | null;\n matched: Array<{ path: string }>;\n}\n\nexport interface RouterLike {\n beforeEach(guard: (to: RouteLike, from: RouteLike) => unknown): () => void;\n afterEach(hook: (to: RouteLike, from: RouteLike, failure?: unknown) => unknown): () => void;\n onError(handler: (error: unknown) => unknown): () => void;\n}\n\n/**\n * The name a route is traced under: its pattern (`/users/:id`), so every user's page is one transaction rather\n * than one per id. Falls back to the route's name, then to its path.\n */\nexport function routeName(route: RouteLike): string {\n // Nuxt's file routes come out as `/users/:id()`; the empty group says nothing, so it is dropped.\n const pattern = route.matched[route.matched.length - 1]?.path?.replace(/\\(\\)/g, '');\n\n return pattern || (typeof route.name === 'string' ? route.name : '') || route.path;\n}\n\n/**\n * Names and times page loads and route changes with Vue Router. The first route gives the page load its pattern;\n * every later navigation is its own `navigation` transaction, which includes the requests it triggers. Errors the\n * router raises (a lazy route whose chunk failed to download) are reported as handled, since the app carries on.\n */\nexport function routerTracing(router: RouterLike): Integration {\n return {\n name: 'VueRouter',\n setup(client: Client) {\n let initial = true;\n\n const removeBefore = router.beforeEach((to) => {\n const name = routeName(to);\n\n if (initial) renameActiveTransaction(client, name);\n else startNavigation(client, name);\n client.scope.setTransactionName(name);\n\n return undefined;\n });\n\n // A cancelled or duplicated navigation is not a failure (Magpie counts any status but \"ok\" as one), so the\n // outcome is not recorded: this only notes that the first route has been seen.\n const removeAfter = router.afterEach(() => {\n initial = false;\n });\n\n const removeError = router.onError((error) => {\n captureException(error, { mechanism: { type: 'vue-router', handled: true } });\n });\n\n return () => {\n removeBefore();\n removeAfter();\n removeError();\n };\n },\n };\n}\n","import { type BrowserOptions, type Client, init as browserInit } from '@greenhat-services/magpie-browser';\nimport type { App } from 'vue';\nimport { attachErrorHandler } from './errorHandler';\nimport { type RouterLike, routerTracing } from './router';\n\nexport * from '@greenhat-services/magpie-browser';\nexport { attachErrorHandler, captureVueError, componentHierarchy, componentName, describeErrorSite } from './errorHandler';\nexport { routeName, routerTracing, type RouteLike, type RouterLike } from './router';\n\nexport const VUE_SDK_NAME = 'magpie.javascript.vue';\n\nexport interface VueOptions extends BrowserOptions {\n /** The app to report errors from. Its `errorHandler` is wrapped, not replaced. */\n app?: App;\n /** The router to name and time page loads and route changes with. */\n router?: RouterLike;\n}\n\n/**\n * Starts Magpie for a Vue 3 app: everything the browser SDK does, plus Vue's own errors (with the component they came\n * from) and, given a router, page loads and navigations named by route pattern.\n *\n * ```ts\n * const app = createApp(App);\n * Magpie.init({ app, router, dsn: import.meta.env.VITE_MAGPIE_DSN, release: import.meta.env.VITE_MAGPIE_RELEASE, tracesSampleRate: 0.2 });\n * app.use(router).mount('#app');\n * ```\n */\nexport function init(options: VueOptions = {}): Client | undefined {\n const { app, router, integrations = [], ...rest } = options;\n\n const client = browserInit({\n ...rest,\n sdkName: rest.sdkName ?? VUE_SDK_NAME,\n integrations: [...(router ? [routerTracing(router)] : []), ...integrations],\n });\n\n if (client && app) attachErrorHandler(app);\n\n return client;\n}\n"]}
@@ -0,0 +1,65 @@
1
+ import { Integration, BrowserOptions, Client } from '@greenhat-services/magpie-browser';
2
+ export * from '@greenhat-services/magpie-browser';
3
+ import { App, ComponentPublicInstance } from 'vue';
4
+
5
+ /** The parts of a Vue Router route this reads. Structural, so the SDK needs no import from `vue-router`. */
6
+ interface RouteLike {
7
+ path: string;
8
+ name?: string | symbol | null;
9
+ matched: Array<{
10
+ path: string;
11
+ }>;
12
+ }
13
+ interface RouterLike {
14
+ beforeEach(guard: (to: RouteLike, from: RouteLike) => unknown): () => void;
15
+ afterEach(hook: (to: RouteLike, from: RouteLike, failure?: unknown) => unknown): () => void;
16
+ onError(handler: (error: unknown) => unknown): () => void;
17
+ }
18
+ /**
19
+ * The name a route is traced under: its pattern (`/users/:id`), so every user's page is one transaction rather
20
+ * than one per id. Falls back to the route's name, then to its path.
21
+ */
22
+ declare function routeName(route: RouteLike): string;
23
+ /**
24
+ * Names and times page loads and route changes with Vue Router. The first route gives the page load its pattern;
25
+ * every later navigation is its own `navigation` transaction, which includes the requests it triggers. Errors the
26
+ * router raises (a lazy route whose chunk failed to download) are reported as handled, since the app carries on.
27
+ */
28
+ declare function routerTracing(router: RouterLike): Integration;
29
+
30
+ /**
31
+ * Where in the component tree a Vue error happened, in words: `UserCard > UserList > App`. Names only, no props or
32
+ * state, which can hold personal data. Capped so a deep tree stays small.
33
+ */
34
+ declare function componentHierarchy(instance: ComponentPublicInstance | null | undefined): string[];
35
+ declare function componentName(instance: ComponentPublicInstance | null | undefined): string;
36
+ declare function describeErrorSite(info: string): string;
37
+ /** Reports an error Vue caught, with the component it came from and the lifecycle step it was in ("setup function", "event handler"). */
38
+ declare function captureVueError(error: unknown, instance: ComponentPublicInstance | null | undefined, info: string): void;
39
+ /**
40
+ * Sets `app.config.errorHandler`, keeping whatever handler the app already had. Vue stops logging errors to the
41
+ * console as soon as a handler exists, so when there was none the error is logged here: Magpie never hides an error
42
+ * from the developer who is looking at the console.
43
+ */
44
+ declare function attachErrorHandler(app: App): void;
45
+
46
+ declare const VUE_SDK_NAME = "magpie.javascript.vue";
47
+ interface VueOptions extends BrowserOptions {
48
+ /** The app to report errors from. Its `errorHandler` is wrapped, not replaced. */
49
+ app?: App;
50
+ /** The router to name and time page loads and route changes with. */
51
+ router?: RouterLike;
52
+ }
53
+ /**
54
+ * Starts Magpie for a Vue 3 app: everything the browser SDK does, plus Vue's own errors (with the component they came
55
+ * from) and, given a router, page loads and navigations named by route pattern.
56
+ *
57
+ * ```ts
58
+ * const app = createApp(App);
59
+ * Magpie.init({ app, router, dsn: import.meta.env.VITE_MAGPIE_DSN, release: import.meta.env.VITE_MAGPIE_RELEASE, tracesSampleRate: 0.2 });
60
+ * app.use(router).mount('#app');
61
+ * ```
62
+ */
63
+ declare function init(options?: VueOptions): Client | undefined;
64
+
65
+ export { type RouteLike, type RouterLike, VUE_SDK_NAME, type VueOptions, attachErrorHandler, captureVueError, componentHierarchy, componentName, describeErrorSite, init, routeName, routerTracing };
@@ -0,0 +1,65 @@
1
+ import { Integration, BrowserOptions, Client } from '@greenhat-services/magpie-browser';
2
+ export * from '@greenhat-services/magpie-browser';
3
+ import { App, ComponentPublicInstance } from 'vue';
4
+
5
+ /** The parts of a Vue Router route this reads. Structural, so the SDK needs no import from `vue-router`. */
6
+ interface RouteLike {
7
+ path: string;
8
+ name?: string | symbol | null;
9
+ matched: Array<{
10
+ path: string;
11
+ }>;
12
+ }
13
+ interface RouterLike {
14
+ beforeEach(guard: (to: RouteLike, from: RouteLike) => unknown): () => void;
15
+ afterEach(hook: (to: RouteLike, from: RouteLike, failure?: unknown) => unknown): () => void;
16
+ onError(handler: (error: unknown) => unknown): () => void;
17
+ }
18
+ /**
19
+ * The name a route is traced under: its pattern (`/users/:id`), so every user's page is one transaction rather
20
+ * than one per id. Falls back to the route's name, then to its path.
21
+ */
22
+ declare function routeName(route: RouteLike): string;
23
+ /**
24
+ * Names and times page loads and route changes with Vue Router. The first route gives the page load its pattern;
25
+ * every later navigation is its own `navigation` transaction, which includes the requests it triggers. Errors the
26
+ * router raises (a lazy route whose chunk failed to download) are reported as handled, since the app carries on.
27
+ */
28
+ declare function routerTracing(router: RouterLike): Integration;
29
+
30
+ /**
31
+ * Where in the component tree a Vue error happened, in words: `UserCard > UserList > App`. Names only, no props or
32
+ * state, which can hold personal data. Capped so a deep tree stays small.
33
+ */
34
+ declare function componentHierarchy(instance: ComponentPublicInstance | null | undefined): string[];
35
+ declare function componentName(instance: ComponentPublicInstance | null | undefined): string;
36
+ declare function describeErrorSite(info: string): string;
37
+ /** Reports an error Vue caught, with the component it came from and the lifecycle step it was in ("setup function", "event handler"). */
38
+ declare function captureVueError(error: unknown, instance: ComponentPublicInstance | null | undefined, info: string): void;
39
+ /**
40
+ * Sets `app.config.errorHandler`, keeping whatever handler the app already had. Vue stops logging errors to the
41
+ * console as soon as a handler exists, so when there was none the error is logged here: Magpie never hides an error
42
+ * from the developer who is looking at the console.
43
+ */
44
+ declare function attachErrorHandler(app: App): void;
45
+
46
+ declare const VUE_SDK_NAME = "magpie.javascript.vue";
47
+ interface VueOptions extends BrowserOptions {
48
+ /** The app to report errors from. Its `errorHandler` is wrapped, not replaced. */
49
+ app?: App;
50
+ /** The router to name and time page loads and route changes with. */
51
+ router?: RouterLike;
52
+ }
53
+ /**
54
+ * Starts Magpie for a Vue 3 app: everything the browser SDK does, plus Vue's own errors (with the component they came
55
+ * from) and, given a router, page loads and navigations named by route pattern.
56
+ *
57
+ * ```ts
58
+ * const app = createApp(App);
59
+ * Magpie.init({ app, router, dsn: import.meta.env.VITE_MAGPIE_DSN, release: import.meta.env.VITE_MAGPIE_RELEASE, tracesSampleRate: 0.2 });
60
+ * app.use(router).mount('#app');
61
+ * ```
62
+ */
63
+ declare function init(options?: VueOptions): Client | undefined;
64
+
65
+ export { type RouteLike, type RouterLike, VUE_SDK_NAME, type VueOptions, attachErrorHandler, captureVueError, componentHierarchy, componentName, describeErrorSite, init, routeName, routerTracing };
package/dist/index.js ADDED
@@ -0,0 +1,124 @@
1
+ import { getClient, captureException, renameActiveTransaction, init as init$1, startNavigation } from '@greenhat-services/magpie-browser';
2
+ export * from '@greenhat-services/magpie-browser';
3
+
4
+ // src/index.ts
5
+ function componentHierarchy(instance) {
6
+ const names = [];
7
+ for (let node = instance, depth = 0; node && depth < 20; node = node.$parent, depth++) {
8
+ names.push(componentName(node));
9
+ }
10
+ return names;
11
+ }
12
+ function componentName(instance) {
13
+ const options = instance?.$options ?? {};
14
+ const file = options.__file?.split(/[\\/]/).pop()?.replace(/\.vue$/, "");
15
+ return options.name || options.__name || file || "Anonymous";
16
+ }
17
+ var VUE_ERROR_SITES = {
18
+ sp: "serverPrefetch hook",
19
+ bc: "beforeCreate hook",
20
+ c: "created hook",
21
+ bm: "beforeMount hook",
22
+ m: "mounted hook",
23
+ bu: "beforeUpdate hook",
24
+ u: "updated hook",
25
+ bum: "beforeUnmount hook",
26
+ um: "unmounted hook",
27
+ a: "activated hook",
28
+ da: "deactivated hook",
29
+ ec: "errorCaptured hook",
30
+ rtc: "renderTracked hook",
31
+ rtg: "renderTriggered hook",
32
+ "0": "setup function",
33
+ "1": "render function",
34
+ "2": "watcher getter",
35
+ "3": "watcher callback",
36
+ "4": "watcher cleanup function",
37
+ "5": "native event handler",
38
+ "6": "component event handler",
39
+ "7": "vnode hook",
40
+ "8": "directive hook",
41
+ "9": "transition hook",
42
+ "10": "app errorHandler",
43
+ "11": "app warnHandler",
44
+ "12": "ref function",
45
+ "13": "async component loader",
46
+ "14": "scheduler flush",
47
+ "15": "component update",
48
+ "16": "app unmount cleanup function"
49
+ };
50
+ function describeErrorSite(info) {
51
+ const code = /#runtime-([a-z0-9]+)$/i.exec(info)?.[1];
52
+ return code && VUE_ERROR_SITES[code] || info;
53
+ }
54
+ function captureVueError(error, instance, info) {
55
+ const client = getClient();
56
+ if (!client) return;
57
+ client.withScope((scope) => {
58
+ const file = instance?.$options?.__file;
59
+ scope.setContext("vue", {
60
+ componentName: componentName(instance),
61
+ lifecycleHook: describeErrorSite(info),
62
+ hierarchy: componentHierarchy(instance).join(" > "),
63
+ ...file ? { file: file.split(/[\\/]/).slice(-3).join("/") } : {}
64
+ });
65
+ scope.setTag("vue.component", componentName(instance));
66
+ captureException(error, { mechanism: { type: "vue", handled: false } });
67
+ });
68
+ }
69
+ function attachErrorHandler(app) {
70
+ const previous = app.config.errorHandler;
71
+ app.config.errorHandler = (error, instance, info) => {
72
+ try {
73
+ captureVueError(error, instance, info);
74
+ } catch {
75
+ }
76
+ if (previous) previous.call(app, error, instance, info);
77
+ else console.error(error);
78
+ };
79
+ }
80
+ function routeName(route) {
81
+ const pattern = route.matched[route.matched.length - 1]?.path?.replace(/\(\)/g, "");
82
+ return pattern || (typeof route.name === "string" ? route.name : "") || route.path;
83
+ }
84
+ function routerTracing(router) {
85
+ return {
86
+ name: "VueRouter",
87
+ setup(client) {
88
+ let initial = true;
89
+ const removeBefore = router.beforeEach((to) => {
90
+ const name = routeName(to);
91
+ if (initial) renameActiveTransaction(client, name);
92
+ else startNavigation(client, name);
93
+ client.scope.setTransactionName(name);
94
+ return void 0;
95
+ });
96
+ const removeAfter = router.afterEach(() => {
97
+ initial = false;
98
+ });
99
+ const removeError = router.onError((error) => {
100
+ captureException(error, { mechanism: { type: "vue-router", handled: true } });
101
+ });
102
+ return () => {
103
+ removeBefore();
104
+ removeAfter();
105
+ removeError();
106
+ };
107
+ }
108
+ };
109
+ }
110
+ var VUE_SDK_NAME = "magpie.javascript.vue";
111
+ function init(options = {}) {
112
+ const { app, router, integrations = [], ...rest } = options;
113
+ const client = init$1({
114
+ ...rest,
115
+ sdkName: rest.sdkName ?? VUE_SDK_NAME,
116
+ integrations: [...router ? [routerTracing(router)] : [], ...integrations]
117
+ });
118
+ if (client && app) attachErrorHandler(app);
119
+ return client;
120
+ }
121
+
122
+ export { VUE_SDK_NAME, attachErrorHandler, captureVueError, componentHierarchy, componentName, describeErrorSite, init, routeName, routerTracing };
123
+ //# sourceMappingURL=index.js.map
124
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errorHandler.ts","../src/router.ts","../src/index.ts"],"names":["captureException","browserInit"],"mappings":"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","file":"index.js","sourcesContent":["import { captureException, getClient } from '@greenhat-services/magpie-browser';\nimport type { App, ComponentPublicInstance } from 'vue';\n\n/**\n * Where in the component tree a Vue error happened, in words: `UserCard > UserList > App`. Names only, no props or\n * state, which can hold personal data. Capped so a deep tree stays small.\n */\nexport function componentHierarchy(instance: ComponentPublicInstance | null | undefined): string[] {\n const names: string[] = [];\n\n for (let node: ComponentPublicInstance | null | undefined = instance, depth = 0; node && depth < 20; node = node.$parent, depth++) {\n names.push(componentName(node));\n }\n\n return names;\n}\n\nexport function componentName(instance: ComponentPublicInstance | null | undefined): string {\n const options = (instance?.$options ?? {}) as { name?: string; __name?: string; __file?: string };\n const file = options.__file?.split(/[\\\\/]/).pop()?.replace(/\\.vue$/, '');\n\n return options.name || options.__name || file || 'Anonymous';\n}\n\n/**\n * Vue's names for where an error happened. Development builds pass these as words; production builds pass a link\n * (`https://vuejs.org/error-reference/#runtime-5`) to save bytes, so the code is turned back into the words here.\n * Numbering is Vue 3.5's `ErrorCodes`; an unknown code is kept as given.\n */\nconst VUE_ERROR_SITES: Record<string, string> = {\n sp: 'serverPrefetch hook', bc: 'beforeCreate hook', c: 'created hook', bm: 'beforeMount hook', m: 'mounted hook',\n bu: 'beforeUpdate hook', u: 'updated hook', bum: 'beforeUnmount hook', um: 'unmounted hook', a: 'activated hook',\n da: 'deactivated hook', ec: 'errorCaptured hook', rtc: 'renderTracked hook', rtg: 'renderTriggered hook',\n '0': 'setup function', '1': 'render function', '2': 'watcher getter', '3': 'watcher callback', '4': 'watcher cleanup function',\n '5': 'native event handler', '6': 'component event handler', '7': 'vnode hook', '8': 'directive hook', '9': 'transition hook',\n '10': 'app errorHandler', '11': 'app warnHandler', '12': 'ref function', '13': 'async component loader', '14': 'scheduler flush',\n '15': 'component update', '16': 'app unmount cleanup function',\n};\n\nexport function describeErrorSite(info: string): string {\n const code = /#runtime-([a-z0-9]+)$/i.exec(info)?.[1];\n\n return (code && VUE_ERROR_SITES[code]) || info;\n}\n\n/** Reports an error Vue caught, with the component it came from and the lifecycle step it was in (\"setup function\", \"event handler\"). */\nexport function captureVueError(error: unknown, instance: ComponentPublicInstance | null | undefined, info: string): void {\n const client = getClient();\n if (!client) return;\n\n client.withScope((scope) => {\n const file = (instance?.$options as { __file?: string } | undefined)?.__file;\n scope.setContext('vue', {\n componentName: componentName(instance),\n lifecycleHook: describeErrorSite(info),\n hierarchy: componentHierarchy(instance).join(' > '),\n ...(file ? { file: file.split(/[\\\\/]/).slice(-3).join('/') } : {}),\n });\n scope.setTag('vue.component', componentName(instance));\n\n captureException(error, { mechanism: { type: 'vue', handled: false } });\n });\n}\n\n/**\n * Sets `app.config.errorHandler`, keeping whatever handler the app already had. Vue stops logging errors to the\n * console as soon as a handler exists, so when there was none the error is logged here: Magpie never hides an error\n * from the developer who is looking at the console.\n */\nexport function attachErrorHandler(app: App): void {\n const previous = app.config.errorHandler;\n\n app.config.errorHandler = (error, instance, info) => {\n try {\n captureVueError(error, instance, info);\n } catch {\n // Reporting must never be the thing that breaks the app.\n }\n\n if (previous) previous.call(app, error, instance, info);\n else console.error(error);\n };\n}\n","import { captureException, type Client, type Integration, renameActiveTransaction, startNavigation } from '@greenhat-services/magpie-browser';\n\n/** The parts of a Vue Router route this reads. Structural, so the SDK needs no import from `vue-router`. */\nexport interface RouteLike {\n path: string;\n name?: string | symbol | null;\n matched: Array<{ path: string }>;\n}\n\nexport interface RouterLike {\n beforeEach(guard: (to: RouteLike, from: RouteLike) => unknown): () => void;\n afterEach(hook: (to: RouteLike, from: RouteLike, failure?: unknown) => unknown): () => void;\n onError(handler: (error: unknown) => unknown): () => void;\n}\n\n/**\n * The name a route is traced under: its pattern (`/users/:id`), so every user's page is one transaction rather\n * than one per id. Falls back to the route's name, then to its path.\n */\nexport function routeName(route: RouteLike): string {\n // Nuxt's file routes come out as `/users/:id()`; the empty group says nothing, so it is dropped.\n const pattern = route.matched[route.matched.length - 1]?.path?.replace(/\\(\\)/g, '');\n\n return pattern || (typeof route.name === 'string' ? route.name : '') || route.path;\n}\n\n/**\n * Names and times page loads and route changes with Vue Router. The first route gives the page load its pattern;\n * every later navigation is its own `navigation` transaction, which includes the requests it triggers. Errors the\n * router raises (a lazy route whose chunk failed to download) are reported as handled, since the app carries on.\n */\nexport function routerTracing(router: RouterLike): Integration {\n return {\n name: 'VueRouter',\n setup(client: Client) {\n let initial = true;\n\n const removeBefore = router.beforeEach((to) => {\n const name = routeName(to);\n\n if (initial) renameActiveTransaction(client, name);\n else startNavigation(client, name);\n client.scope.setTransactionName(name);\n\n return undefined;\n });\n\n // A cancelled or duplicated navigation is not a failure (Magpie counts any status but \"ok\" as one), so the\n // outcome is not recorded: this only notes that the first route has been seen.\n const removeAfter = router.afterEach(() => {\n initial = false;\n });\n\n const removeError = router.onError((error) => {\n captureException(error, { mechanism: { type: 'vue-router', handled: true } });\n });\n\n return () => {\n removeBefore();\n removeAfter();\n removeError();\n };\n },\n };\n}\n","import { type BrowserOptions, type Client, init as browserInit } from '@greenhat-services/magpie-browser';\nimport type { App } from 'vue';\nimport { attachErrorHandler } from './errorHandler';\nimport { type RouterLike, routerTracing } from './router';\n\nexport * from '@greenhat-services/magpie-browser';\nexport { attachErrorHandler, captureVueError, componentHierarchy, componentName, describeErrorSite } from './errorHandler';\nexport { routeName, routerTracing, type RouteLike, type RouterLike } from './router';\n\nexport const VUE_SDK_NAME = 'magpie.javascript.vue';\n\nexport interface VueOptions extends BrowserOptions {\n /** The app to report errors from. Its `errorHandler` is wrapped, not replaced. */\n app?: App;\n /** The router to name and time page loads and route changes with. */\n router?: RouterLike;\n}\n\n/**\n * Starts Magpie for a Vue 3 app: everything the browser SDK does, plus Vue's own errors (with the component they came\n * from) and, given a router, page loads and navigations named by route pattern.\n *\n * ```ts\n * const app = createApp(App);\n * Magpie.init({ app, router, dsn: import.meta.env.VITE_MAGPIE_DSN, release: import.meta.env.VITE_MAGPIE_RELEASE, tracesSampleRate: 0.2 });\n * app.use(router).mount('#app');\n * ```\n */\nexport function init(options: VueOptions = {}): Client | undefined {\n const { app, router, integrations = [], ...rest } = options;\n\n const client = browserInit({\n ...rest,\n sdkName: rest.sdkName ?? VUE_SDK_NAME,\n integrations: [...(router ? [routerTracing(router)] : []), ...integrations],\n });\n\n if (client && app) attachErrorHandler(app);\n\n return client;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
- "name": "@greenhat-services/magpie-vue",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@greenhat-services/magpie-vue",
3
+ "version": "0.1.1",
4
+ "description": "Magpie error tracking and performance monitoring for Vue 3 and Vue Router.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "prepack": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "@greenhat-services/magpie-browser": "^0.1.0"
27
+ },
28
+ "peerDependencies": {
29
+ "vue": "^3.3.0"
30
+ },
31
+ "devDependencies": {
32
+ "vue": "^3.5.0",
33
+ "vue-router": "^4.5.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/Greenhat-Services/magpie-js.git",
41
+ "directory": "packages/vue"
42
+ },
43
+ "keywords": [
44
+ "magpie",
45
+ "error-tracking",
46
+ "monitoring",
47
+ "vue",
48
+ "vue-router"
49
+ ]
50
+ }