@goliapkg/sentori-vue 0.1.0
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/lib/ErrorBoundary.d.ts +22 -0
- package/lib/ErrorBoundary.d.ts.map +1 -0
- package/lib/ErrorBoundary.js +48 -0
- package/lib/ErrorBoundary.js.map +1 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +55 -0
- package/lib/index.js.map +1 -0
- package/lib/router.d.ts +17 -0
- package/lib/router.d.ts.map +1 -0
- package/lib/router.js +46 -0
- package/lib/router.js.map +1 -0
- package/package.json +67 -0
- package/src/ErrorBoundary.ts +49 -0
- package/src/__tests__/smoke.test.ts +30 -0
- package/src/index.ts +73 -0
- package/src/router.ts +55 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const SentoriErrorBoundary: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
2
|
+
/** Optional list of error names (`error.name`) to ignore — they
|
|
3
|
+
* pass through to upper boundaries unchanged. */
|
|
4
|
+
ignore: {
|
|
5
|
+
type: () => readonly string[];
|
|
6
|
+
default: () => never[];
|
|
7
|
+
};
|
|
8
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}> | import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}>[] | undefined, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
13
|
+
/** Optional list of error names (`error.name`) to ignore — they
|
|
14
|
+
* pass through to upper boundaries unchanged. */
|
|
15
|
+
ignore: {
|
|
16
|
+
type: () => readonly string[];
|
|
17
|
+
default: () => never[];
|
|
18
|
+
};
|
|
19
|
+
}>> & Readonly<{}>, {
|
|
20
|
+
ignore: readonly string[];
|
|
21
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
22
|
+
//# sourceMappingURL=ErrorBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorBoundary.d.ts","sourceRoot":"","sources":["../src/ErrorBoundary.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,oBAAoB;IAG7B;sDACkD;;cACzB,MAAM,SAAS,MAAM,EAAE;;;;;;;;IAFhD;sDACkD;;cACzB,MAAM,SAAS,MAAM,EAAE;;;;;4EA+BlD,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Phase 45 sub-B — Vue error boundary component.
|
|
2
|
+
//
|
|
3
|
+
// Vue 3 doesn't ship a built-in ErrorBoundary like React. The
|
|
4
|
+
// pattern is to use the `errorCaptured(err, instance, info)`
|
|
5
|
+
// lifecycle hook on a wrapper component that returns `false` to
|
|
6
|
+
// stop the error from propagating. Our wrapper captures into
|
|
7
|
+
// Sentori and renders either the slot's children or a `fallback`
|
|
8
|
+
// slot when the subtree threw.
|
|
9
|
+
import { captureException } from '@goliapkg/sentori-javascript';
|
|
10
|
+
import { defineComponent, h, ref } from 'vue';
|
|
11
|
+
export const SentoriErrorBoundary = defineComponent({
|
|
12
|
+
name: 'SentoriErrorBoundary',
|
|
13
|
+
props: {
|
|
14
|
+
/** Optional list of error names (`error.name`) to ignore — they
|
|
15
|
+
* pass through to upper boundaries unchanged. */
|
|
16
|
+
ignore: { type: Array, default: () => [] },
|
|
17
|
+
},
|
|
18
|
+
setup(_props, { slots }) {
|
|
19
|
+
const caughtError = ref(null);
|
|
20
|
+
const reset = () => {
|
|
21
|
+
caughtError.value = null;
|
|
22
|
+
};
|
|
23
|
+
return () => {
|
|
24
|
+
if (caughtError.value) {
|
|
25
|
+
if (slots.fallback) {
|
|
26
|
+
return slots.fallback({ error: caughtError.value, reset });
|
|
27
|
+
}
|
|
28
|
+
// Default fallback: a hidden span so Vue doesn't render a
|
|
29
|
+
// crashed subtree. Apps that don't pass a fallback opt in
|
|
30
|
+
// to that minimal behaviour.
|
|
31
|
+
return h('span', { 'data-sentori-boundary-error': 'true' });
|
|
32
|
+
}
|
|
33
|
+
return slots.default?.();
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
errorCaptured(err, _instance, info) {
|
|
37
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
38
|
+
if (this.ignore.includes(e.name)) {
|
|
39
|
+
return true; // propagate further
|
|
40
|
+
}
|
|
41
|
+
captureException(e, { tags: { 'vue.errorInfo': info } });
|
|
42
|
+
// Switch to fallback render and stop propagation.
|
|
43
|
+
this.$forceUpdate();
|
|
44
|
+
this.caughtError = { value: e };
|
|
45
|
+
return false;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
//# sourceMappingURL=ErrorBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ErrorBoundary.js","sourceRoot":"","sources":["../src/ErrorBoundary.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,8DAA8D;AAC9D,6DAA6D;AAC7D,gEAAgE;AAChE,6DAA6D;AAC7D,iEAAiE;AACjE,+BAA+B;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,OAAO,EAAE,eAAe,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAE7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAC;IAClD,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE;QACL;0DACkD;QAClD,MAAM,EAAE,EAAE,IAAI,EAAE,KAAgC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE;KACtE;IACD,KAAK,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE;QACrB,MAAM,WAAW,GAAG,GAAG,CAAe,IAAI,CAAC,CAAA;QAC3C,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,WAAW,CAAC,KAAK,GAAG,IAAI,CAAA;QAC1B,CAAC,CAAA;QACD,OAAO,GAAG,EAAE;YACV,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC;gBACtB,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;oBACnB,OAAO,KAAK,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;gBAC5D,CAAC;gBACD,0DAA0D;gBAC1D,0DAA0D;gBAC1D,6BAA6B;gBAC7B,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,6BAA6B,EAAE,MAAM,EAAE,CAAC,CAAA;YAC7D,CAAC;YACD,OAAO,KAAK,CAAC,OAAO,EAAE,EAAE,CAAA;QAC1B,CAAC,CAAA;IACH,CAAC;IACD,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI;QAChC,MAAM,CAAC,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAC7D,IAAK,IAAI,CAAC,MAA4B,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACxD,OAAO,IAAI,CAAA,CAAC,oBAAoB;QAClC,CAAC;QACD,gBAAgB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACxD,kDAAkD;QAClD,IAAI,CAAC,YAAY,EAAE,CAClB;QAAC,IAA4D,CAAC,WAAW,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;QACzF,OAAO,KAAK,CAAA;IACd,CAAC;CACF,CAAC,CAAA"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 45 sub-B — Vue 3 adapter for Sentori.
|
|
3
|
+
*
|
|
4
|
+
* Plugin shape:
|
|
5
|
+
*
|
|
6
|
+
* import { createApp } from 'vue'
|
|
7
|
+
* import sentori from '@goliapkg/sentori-vue'
|
|
8
|
+
*
|
|
9
|
+
* const app = createApp(App)
|
|
10
|
+
* app.use(sentori, {
|
|
11
|
+
* token: 'st_pk_…',
|
|
12
|
+
* release: 'myapp@1.0.0',
|
|
13
|
+
* sampling: { errors: 1.0 },
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* What `app.use(sentori, opts)` does:
|
|
17
|
+
* 1. forwards `opts` to `@goliapkg/sentori-javascript`'s init
|
|
18
|
+
* 2. wires `app.config.errorHandler` so any error thrown inside
|
|
19
|
+
* a render / lifecycle bubbles into `captureException`
|
|
20
|
+
* 3. tags every Sentori event with `tags.vue.version` so the
|
|
21
|
+
* dashboard knows which framework is producing the data
|
|
22
|
+
*
|
|
23
|
+
* Router integration (Vue Router) lives in the `/router` subpath:
|
|
24
|
+
*
|
|
25
|
+
* import { setupTraceNavigation } from '@goliapkg/sentori-vue/router'
|
|
26
|
+
* setupTraceNavigation(router)
|
|
27
|
+
*/
|
|
28
|
+
import type { Plugin } from 'vue';
|
|
29
|
+
import { type InitOptions } from '@goliapkg/sentori-javascript';
|
|
30
|
+
export type SentoriVueOptions = InitOptions;
|
|
31
|
+
declare const plugin: Plugin;
|
|
32
|
+
export default plugin;
|
|
33
|
+
export { plugin as sentori };
|
|
34
|
+
export { addBreadcrumb, captureException, captureException as captureError, captureStep, getUser, setUser, } from '@goliapkg/sentori-javascript';
|
|
35
|
+
export { SentoriErrorBoundary } from './ErrorBoundary.js';
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAO,MAAM,EAAE,MAAM,KAAK,CAAA;AACtC,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAA;AAErC,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAA;AAE3C,QAAA,MAAM,MAAM,EAAE,MAqBb,CAAA;AAED,eAAe,MAAM,CAAA;AACrB,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,CAAA;AAE5B,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,gBAAgB,IAAI,YAAY,EAChC,WAAW,EACX,OAAO,EACP,OAAO,GACR,MAAM,8BAA8B,CAAA;AAErC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 45 sub-B — Vue 3 adapter for Sentori.
|
|
3
|
+
*
|
|
4
|
+
* Plugin shape:
|
|
5
|
+
*
|
|
6
|
+
* import { createApp } from 'vue'
|
|
7
|
+
* import sentori from '@goliapkg/sentori-vue'
|
|
8
|
+
*
|
|
9
|
+
* const app = createApp(App)
|
|
10
|
+
* app.use(sentori, {
|
|
11
|
+
* token: 'st_pk_…',
|
|
12
|
+
* release: 'myapp@1.0.0',
|
|
13
|
+
* sampling: { errors: 1.0 },
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* What `app.use(sentori, opts)` does:
|
|
17
|
+
* 1. forwards `opts` to `@goliapkg/sentori-javascript`'s init
|
|
18
|
+
* 2. wires `app.config.errorHandler` so any error thrown inside
|
|
19
|
+
* a render / lifecycle bubbles into `captureException`
|
|
20
|
+
* 3. tags every Sentori event with `tags.vue.version` so the
|
|
21
|
+
* dashboard knows which framework is producing the data
|
|
22
|
+
*
|
|
23
|
+
* Router integration (Vue Router) lives in the `/router` subpath:
|
|
24
|
+
*
|
|
25
|
+
* import { setupTraceNavigation } from '@goliapkg/sentori-vue/router'
|
|
26
|
+
* setupTraceNavigation(router)
|
|
27
|
+
*/
|
|
28
|
+
import { captureException as captureExceptionJs, initSentori as initSentoriJs, } from '@goliapkg/sentori-javascript';
|
|
29
|
+
const plugin = {
|
|
30
|
+
install(app, options) {
|
|
31
|
+
// 1. init the core JS SDK.
|
|
32
|
+
initSentoriJs(options);
|
|
33
|
+
// 2. Vue's global error handler. Sentori captureException
|
|
34
|
+
// accepts an Error; Vue's handler receives `unknown`. Wrap
|
|
35
|
+
// non-Error values so the SDK still gets a stack.
|
|
36
|
+
const previous = app.config.errorHandler;
|
|
37
|
+
app.config.errorHandler = (err, instance, info) => {
|
|
38
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
39
|
+
captureExceptionJs(e, {
|
|
40
|
+
tags: {
|
|
41
|
+
'vue.component': instance?.$options?.name ?? '<anonymous>',
|
|
42
|
+
'vue.errorInfo': info,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
// Chain to any previously installed handler so plugins layer.
|
|
46
|
+
if (previous)
|
|
47
|
+
previous(err, instance, info);
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export default plugin;
|
|
52
|
+
export { plugin as sentori };
|
|
53
|
+
export { addBreadcrumb, captureException, captureException as captureError, captureStep, getUser, setUser, } from '@goliapkg/sentori-javascript';
|
|
54
|
+
export { SentoriErrorBoundary } from './ErrorBoundary.js';
|
|
55
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAGH,OAAO,EACL,gBAAgB,IAAI,kBAAkB,EACtC,WAAW,IAAI,aAAa,GAE7B,MAAM,8BAA8B,CAAA;AAIrC,MAAM,MAAM,GAAW;IACrB,OAAO,CAAC,GAAQ,EAAE,OAA0B;QAC1C,2BAA2B;QAC3B,aAAa,CAAC,OAAO,CAAC,CAAA;QAEtB,0DAA0D;QAC1D,8DAA8D;QAC9D,qDAAqD;QACrD,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,YAAY,CAAA;QACxC,GAAG,CAAC,MAAM,CAAC,YAAY,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;YAChD,MAAM,CAAC,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAC7D,kBAAkB,CAAC,CAAC,EAAE;gBACpB,IAAI,EAAE;oBACJ,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,IAAI,aAAa;oBAC1D,eAAe,EAAE,IAAI;iBACtB;aACF,CAAC,CAAA;YACF,8DAA8D;YAC9D,IAAI,QAAQ;gBAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;QAC7C,CAAC,CAAA;IACH,CAAC;CACF,CAAA;AAED,eAAe,MAAM,CAAA;AACrB,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,CAAA;AAE5B,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,gBAAgB,IAAI,YAAY,EAChC,WAAW,EACX,OAAO,EACP,OAAO,GACR,MAAM,8BAA8B,CAAA;AAErC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/lib/router.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type RouterLike = {
|
|
2
|
+
beforeEach: (cb: (to: {
|
|
3
|
+
path: string;
|
|
4
|
+
name?: unknown;
|
|
5
|
+
}, from: {
|
|
6
|
+
path: string;
|
|
7
|
+
}) => void) => void;
|
|
8
|
+
afterEach: (cb: (to: {
|
|
9
|
+
path: string;
|
|
10
|
+
name?: unknown;
|
|
11
|
+
}, from: {
|
|
12
|
+
path: string;
|
|
13
|
+
}) => void) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function setupTraceNavigation(router: RouterLike): void;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAmBA,KAAK,UAAU,GAAG;IAChB,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,IAAI,CAAA;IAChG,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,IAAI,CAAA;CAChG,CAAA;AAID,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CA4B7D"}
|
package/lib/router.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Phase 45 sub-B — Vue Router auto-trace navigation.
|
|
2
|
+
//
|
|
3
|
+
// import { createRouter } from 'vue-router'
|
|
4
|
+
// import { setupTraceNavigation } from '@goliapkg/sentori-vue/router'
|
|
5
|
+
//
|
|
6
|
+
// const router = createRouter({ ... })
|
|
7
|
+
// setupTraceNavigation(router)
|
|
8
|
+
//
|
|
9
|
+
// On every route push, we open a `vue.navigation` span keyed by the
|
|
10
|
+
// destination path, mark it active, and finish it on the next
|
|
11
|
+
// `afterEach`. Sentori spans / fetch / xhr instrumentation in
|
|
12
|
+
// `@goliapkg/sentori-javascript` automatically nest into it so each
|
|
13
|
+
// screen's network requests cluster into one trace.
|
|
14
|
+
import { setActiveSpan, startSpan } from '@goliapkg/sentori-core';
|
|
15
|
+
import { captureStep } from '@goliapkg/sentori-javascript';
|
|
16
|
+
let _active = null;
|
|
17
|
+
export function setupTraceNavigation(router) {
|
|
18
|
+
router.beforeEach((to, from) => {
|
|
19
|
+
// Finish any still-open span from the previous transition that
|
|
20
|
+
// afterEach didn't reach (route guard rejected, etc.).
|
|
21
|
+
if (_active) {
|
|
22
|
+
_active.finish({ status: 'ok' });
|
|
23
|
+
_active = null;
|
|
24
|
+
}
|
|
25
|
+
const name = `${from.path || '/'} → ${to.path || '/'}`;
|
|
26
|
+
const span = startSpan('vue.navigation', {
|
|
27
|
+
name,
|
|
28
|
+
parent: null,
|
|
29
|
+
tags: { 'nav.from': from.path || '/', 'nav.to': to.path || '/' },
|
|
30
|
+
});
|
|
31
|
+
_active = span;
|
|
32
|
+
setActiveSpan(span);
|
|
33
|
+
// Phase 46 — also record into the session-trail buffer; no-op
|
|
34
|
+
// unless `init({ capture: { sessionTrail: true } })`.
|
|
35
|
+
captureStep(`route:${to.path || '/'}`, {
|
|
36
|
+
breadcrumb: { type: 'navigation', message: name },
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
router.afterEach(() => {
|
|
40
|
+
if (_active) {
|
|
41
|
+
_active.finish({ status: 'ok' });
|
|
42
|
+
_active = null;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,EAAE;AACF,gDAAgD;AAChD,0EAA0E;AAC1E,EAAE;AACF,2CAA2C;AAC3C,mCAAmC;AACnC,EAAE;AACF,oEAAoE;AACpE,8DAA8D;AAC9D,8DAA8D;AAC9D,oEAAoE;AACpE,oDAAoD;AAEpD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAmB,MAAM,wBAAwB,CAAA;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAS1D,IAAI,OAAO,GAAsB,IAAI,CAAA;AAErC,MAAM,UAAU,oBAAoB,CAAC,MAAkB;IACrD,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE;QAC7B,+DAA+D;QAC/D,uDAAuD;QACvD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;YAChC,OAAO,GAAG,IAAI,CAAA;QAChB,CAAC;QACD,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,IAAI,GAAG,EAAE,CAAA;QACtD,MAAM,IAAI,GAAG,SAAS,CAAC,gBAAgB,EAAE;YACvC,IAAI;YACJ,MAAM,EAAE,IAAI;YACZ,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,IAAI,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,EAAE;SACjE,CAAC,CAAA;QACF,OAAO,GAAG,IAAI,CAAA;QACd,aAAa,CAAC,IAAI,CAAC,CAAA;QACnB,8DAA8D;QAC9D,sDAAsD;QACtD,WAAW,CAAC,SAAS,EAAE,CAAC,IAAI,IAAI,GAAG,EAAE,EAAE;YACrC,UAAU,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE;SAClD,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE;QACpB,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;YAChC,OAAO,GAAG,IAAI,CAAA;QAChB,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goliapkg/sentori-vue",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vue 3 adapter for Sentori — plugin, errorHandler hook, Vue Router auto-trace navigation, <SentoriErrorBoundary>.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://sentori.golia.jp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/goliajp/sentori.git",
|
|
10
|
+
"directory": "sdk/vue"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/goliajp/sentori/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"sentori",
|
|
17
|
+
"vue",
|
|
18
|
+
"vue3",
|
|
19
|
+
"error-tracking",
|
|
20
|
+
"error-boundary"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./lib/index.js",
|
|
24
|
+
"types": "./lib/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./lib/index.d.ts",
|
|
28
|
+
"default": "./lib/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./router": {
|
|
31
|
+
"types": "./lib/router.d.ts",
|
|
32
|
+
"default": "./lib/router.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib/",
|
|
37
|
+
"src/",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"prepack": "bun run build"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"vue": ">=3.4",
|
|
48
|
+
"vue-router": ">=4"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"vue-router": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@goliapkg/sentori-core": "0.6.0",
|
|
57
|
+
"@goliapkg/sentori-javascript": "0.4.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/bun": "latest",
|
|
61
|
+
"typescript": "^5",
|
|
62
|
+
"vue": "^3.4"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Phase 45 sub-B — Vue error boundary component.
|
|
2
|
+
//
|
|
3
|
+
// Vue 3 doesn't ship a built-in ErrorBoundary like React. The
|
|
4
|
+
// pattern is to use the `errorCaptured(err, instance, info)`
|
|
5
|
+
// lifecycle hook on a wrapper component that returns `false` to
|
|
6
|
+
// stop the error from propagating. Our wrapper captures into
|
|
7
|
+
// Sentori and renders either the slot's children or a `fallback`
|
|
8
|
+
// slot when the subtree threw.
|
|
9
|
+
|
|
10
|
+
import { captureException } from '@goliapkg/sentori-javascript'
|
|
11
|
+
import { defineComponent, h, ref } from 'vue'
|
|
12
|
+
|
|
13
|
+
export const SentoriErrorBoundary = defineComponent({
|
|
14
|
+
name: 'SentoriErrorBoundary',
|
|
15
|
+
props: {
|
|
16
|
+
/** Optional list of error names (`error.name`) to ignore — they
|
|
17
|
+
* pass through to upper boundaries unchanged. */
|
|
18
|
+
ignore: { type: Array as () => readonly string[], default: () => [] },
|
|
19
|
+
},
|
|
20
|
+
setup(_props, { slots }) {
|
|
21
|
+
const caughtError = ref<Error | null>(null)
|
|
22
|
+
const reset = () => {
|
|
23
|
+
caughtError.value = null
|
|
24
|
+
}
|
|
25
|
+
return () => {
|
|
26
|
+
if (caughtError.value) {
|
|
27
|
+
if (slots.fallback) {
|
|
28
|
+
return slots.fallback({ error: caughtError.value, reset })
|
|
29
|
+
}
|
|
30
|
+
// Default fallback: a hidden span so Vue doesn't render a
|
|
31
|
+
// crashed subtree. Apps that don't pass a fallback opt in
|
|
32
|
+
// to that minimal behaviour.
|
|
33
|
+
return h('span', { 'data-sentori-boundary-error': 'true' })
|
|
34
|
+
}
|
|
35
|
+
return slots.default?.()
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
errorCaptured(err, _instance, info) {
|
|
39
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
40
|
+
if ((this.ignore as readonly string[]).includes(e.name)) {
|
|
41
|
+
return true // propagate further
|
|
42
|
+
}
|
|
43
|
+
captureException(e, { tags: { 'vue.errorInfo': info } })
|
|
44
|
+
// Switch to fallback render and stop propagation.
|
|
45
|
+
this.$forceUpdate()
|
|
46
|
+
;(this as unknown as { caughtError: { value: Error | null } }).caughtError = { value: e }
|
|
47
|
+
return false
|
|
48
|
+
},
|
|
49
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import plugin, {
|
|
4
|
+
SentoriErrorBoundary,
|
|
5
|
+
addBreadcrumb,
|
|
6
|
+
captureException,
|
|
7
|
+
sentori,
|
|
8
|
+
} from '../index.js'
|
|
9
|
+
import { setupTraceNavigation } from '../router.js'
|
|
10
|
+
|
|
11
|
+
describe('@goliapkg/sentori-vue exports', () => {
|
|
12
|
+
test('default export is a Vue plugin with an install function', () => {
|
|
13
|
+
expect(typeof plugin.install).toBe('function')
|
|
14
|
+
expect(plugin).toBe(sentori)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('SentoriErrorBoundary is a defineComponent', () => {
|
|
18
|
+
expect(SentoriErrorBoundary).toBeDefined()
|
|
19
|
+
expect(typeof SentoriErrorBoundary).toBe('object')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('Vue Router helper is exported', () => {
|
|
23
|
+
expect(typeof setupTraceNavigation).toBe('function')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('re-exports common SDK helpers', () => {
|
|
27
|
+
expect(typeof captureException).toBe('function')
|
|
28
|
+
expect(typeof addBreadcrumb).toBe('function')
|
|
29
|
+
})
|
|
30
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 45 sub-B — Vue 3 adapter for Sentori.
|
|
3
|
+
*
|
|
4
|
+
* Plugin shape:
|
|
5
|
+
*
|
|
6
|
+
* import { createApp } from 'vue'
|
|
7
|
+
* import sentori from '@goliapkg/sentori-vue'
|
|
8
|
+
*
|
|
9
|
+
* const app = createApp(App)
|
|
10
|
+
* app.use(sentori, {
|
|
11
|
+
* token: 'st_pk_…',
|
|
12
|
+
* release: 'myapp@1.0.0',
|
|
13
|
+
* sampling: { errors: 1.0 },
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* What `app.use(sentori, opts)` does:
|
|
17
|
+
* 1. forwards `opts` to `@goliapkg/sentori-javascript`'s init
|
|
18
|
+
* 2. wires `app.config.errorHandler` so any error thrown inside
|
|
19
|
+
* a render / lifecycle bubbles into `captureException`
|
|
20
|
+
* 3. tags every Sentori event with `tags.vue.version` so the
|
|
21
|
+
* dashboard knows which framework is producing the data
|
|
22
|
+
*
|
|
23
|
+
* Router integration (Vue Router) lives in the `/router` subpath:
|
|
24
|
+
*
|
|
25
|
+
* import { setupTraceNavigation } from '@goliapkg/sentori-vue/router'
|
|
26
|
+
* setupTraceNavigation(router)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { App, Plugin } from 'vue'
|
|
30
|
+
import {
|
|
31
|
+
captureException as captureExceptionJs,
|
|
32
|
+
initSentori as initSentoriJs,
|
|
33
|
+
type InitOptions,
|
|
34
|
+
} from '@goliapkg/sentori-javascript'
|
|
35
|
+
|
|
36
|
+
export type SentoriVueOptions = InitOptions
|
|
37
|
+
|
|
38
|
+
const plugin: Plugin = {
|
|
39
|
+
install(app: App, options: SentoriVueOptions) {
|
|
40
|
+
// 1. init the core JS SDK.
|
|
41
|
+
initSentoriJs(options)
|
|
42
|
+
|
|
43
|
+
// 2. Vue's global error handler. Sentori captureException
|
|
44
|
+
// accepts an Error; Vue's handler receives `unknown`. Wrap
|
|
45
|
+
// non-Error values so the SDK still gets a stack.
|
|
46
|
+
const previous = app.config.errorHandler
|
|
47
|
+
app.config.errorHandler = (err, instance, info) => {
|
|
48
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
49
|
+
captureExceptionJs(e, {
|
|
50
|
+
tags: {
|
|
51
|
+
'vue.component': instance?.$options?.name ?? '<anonymous>',
|
|
52
|
+
'vue.errorInfo': info,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
// Chain to any previously installed handler so plugins layer.
|
|
56
|
+
if (previous) previous(err, instance, info)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default plugin
|
|
62
|
+
export { plugin as sentori }
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
addBreadcrumb,
|
|
66
|
+
captureException,
|
|
67
|
+
captureException as captureError,
|
|
68
|
+
captureStep,
|
|
69
|
+
getUser,
|
|
70
|
+
setUser,
|
|
71
|
+
} from '@goliapkg/sentori-javascript'
|
|
72
|
+
|
|
73
|
+
export { SentoriErrorBoundary } from './ErrorBoundary.js'
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Phase 45 sub-B — Vue Router auto-trace navigation.
|
|
2
|
+
//
|
|
3
|
+
// import { createRouter } from 'vue-router'
|
|
4
|
+
// import { setupTraceNavigation } from '@goliapkg/sentori-vue/router'
|
|
5
|
+
//
|
|
6
|
+
// const router = createRouter({ ... })
|
|
7
|
+
// setupTraceNavigation(router)
|
|
8
|
+
//
|
|
9
|
+
// On every route push, we open a `vue.navigation` span keyed by the
|
|
10
|
+
// destination path, mark it active, and finish it on the next
|
|
11
|
+
// `afterEach`. Sentori spans / fetch / xhr instrumentation in
|
|
12
|
+
// `@goliapkg/sentori-javascript` automatically nest into it so each
|
|
13
|
+
// screen's network requests cluster into one trace.
|
|
14
|
+
|
|
15
|
+
import { setActiveSpan, startSpan, type SpanHandle } from '@goliapkg/sentori-core'
|
|
16
|
+
import { captureStep } from '@goliapkg/sentori-javascript'
|
|
17
|
+
|
|
18
|
+
// Minimal duck type — accept anything that exposes `beforeEach` +
|
|
19
|
+
// `afterEach`. Avoids hard-coding a vue-router version.
|
|
20
|
+
type RouterLike = {
|
|
21
|
+
beforeEach: (cb: (to: { path: string; name?: unknown }, from: { path: string }) => void) => void
|
|
22
|
+
afterEach: (cb: (to: { path: string; name?: unknown }, from: { path: string }) => void) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let _active: SpanHandle | null = null
|
|
26
|
+
|
|
27
|
+
export function setupTraceNavigation(router: RouterLike): void {
|
|
28
|
+
router.beforeEach((to, from) => {
|
|
29
|
+
// Finish any still-open span from the previous transition that
|
|
30
|
+
// afterEach didn't reach (route guard rejected, etc.).
|
|
31
|
+
if (_active) {
|
|
32
|
+
_active.finish({ status: 'ok' })
|
|
33
|
+
_active = null
|
|
34
|
+
}
|
|
35
|
+
const name = `${from.path || '/'} → ${to.path || '/'}`
|
|
36
|
+
const span = startSpan('vue.navigation', {
|
|
37
|
+
name,
|
|
38
|
+
parent: null,
|
|
39
|
+
tags: { 'nav.from': from.path || '/', 'nav.to': to.path || '/' },
|
|
40
|
+
})
|
|
41
|
+
_active = span
|
|
42
|
+
setActiveSpan(span)
|
|
43
|
+
// Phase 46 — also record into the session-trail buffer; no-op
|
|
44
|
+
// unless `init({ capture: { sessionTrail: true } })`.
|
|
45
|
+
captureStep(`route:${to.path || '/'}`, {
|
|
46
|
+
breadcrumb: { type: 'navigation', message: name },
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
router.afterEach(() => {
|
|
50
|
+
if (_active) {
|
|
51
|
+
_active.finish({ status: 'ok' })
|
|
52
|
+
_active = null
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|