@flareapp/vue 2.6.0 → 2.8.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/README.md +47 -0
- package/dist/{FlareErrorBoundary-G0Jqt8zN.cjs → FlareErrorBoundary-BndkpJFI.cjs} +215 -92
- package/dist/{FlareErrorBoundary-9LmNTGFB.mjs → FlareErrorBoundary-CO0ZV9lZ.mjs} +216 -93
- package/dist/{constants-B6O_8KY6.d.cts → constants-BFiYPDOl.d.cts} +9 -1
- package/dist/{constants-DCI9dIQ_.d.mts → constants-CFWxnFRO.d.mts} +10 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/inject.cjs +1 -1
- package/dist/inject.d.cts +1 -1
- package/dist/inject.d.mts +1 -1
- package/dist/inject.mjs +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -54,6 +54,53 @@ See the [JavaScript identifying-users docs](https://flareapp.io/docs/javascript/
|
|
|
54
54
|
|
|
55
55
|
Full documentation on the Vue error handler and its options is available at [flareapp.io/docs/vue/general/installation](https://flareapp.io/docs/vue/general/installation).
|
|
56
56
|
|
|
57
|
+
## Component profiling
|
|
58
|
+
|
|
59
|
+
Record a span per component mount, nested under the active page-load or navigation trace. Requires tracing
|
|
60
|
+
(`enableTracing: true`) and an allowlist:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
app.use(flareVue, {
|
|
64
|
+
router,
|
|
65
|
+
profileComponents: ['ProductPage', 'CartPage', /^Checkout/],
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`app.use(flareVue, …)` and `flare.configure({ enableTracing: true })` can run in either order. Installing
|
|
70
|
+
the plugin first is fine: the router guards and the profiler hook stay idle until tracing is on.
|
|
71
|
+
|
|
72
|
+
Strings match the component name exactly. Regular expressions match by `test()`. `true` profiles every named
|
|
73
|
+
component, which is useful when exploring but will hit the 1024 span per trace cap on a real page and bury the
|
|
74
|
+
spans you care about.
|
|
75
|
+
|
|
76
|
+
Names come from the same resolution the error reports use: the name the SFC compiler derives from the filename,
|
|
77
|
+
then an explicit `name` option, then `AnonymousComponent`. Renaming a component silently stops profiling it.
|
|
78
|
+
|
|
79
|
+
Only mounts are recorded. Updates, `<KeepAlive>` reactivation and unmounts are not.
|
|
80
|
+
|
|
81
|
+
A component that mounts later inside an already-mounted profiled ancestor still nests under that ancestor, whose
|
|
82
|
+
own span closed when it finished mounting. The tree is correct, but the waterfall shows the child starting after
|
|
83
|
+
its parent ended. A page body swapped inside a persistent layout is the usual way to see this.
|
|
84
|
+
|
|
85
|
+
**Async components and `<Suspense>`:** Vue treats a component as mounted once its _synchronous_ children
|
|
86
|
+
are mounted. A component with an async `setup()`, or one inside a `<Suspense>` boundary, therefore mounts
|
|
87
|
+
after its profiled ancestor's span has closed, so it appears after its parent in the waterfall. If the
|
|
88
|
+
whole trace closed by then, the span is dropped rather than attached to a finished trace.
|
|
89
|
+
|
|
90
|
+
### Naming with Inertia
|
|
91
|
+
|
|
92
|
+
Inertia names navigation spans after the page component from its page object, so a root reads `Products/Show`.
|
|
93
|
+
Vue names components after the file, so `./Pages/Products/Show.vue` is `Show`, and `./Pages/Orders/Show.vue` is
|
|
94
|
+
also `Show`. Set the name explicitly on page components if you want the two to read the same:
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<script setup>
|
|
98
|
+
defineOptions({ name: 'Products/Show' });
|
|
99
|
+
</script>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Without it, spans use the bare filename, which is usually still readable because the root span disambiguates.
|
|
103
|
+
|
|
57
104
|
## Compatibility
|
|
58
105
|
|
|
59
106
|
- Vue 3
|
|
@@ -1,31 +1,14 @@
|
|
|
1
|
+
let _flareapp_js_browser = require("@flareapp/js/browser");
|
|
1
2
|
let _flareapp_core = require("@flareapp/core");
|
|
2
3
|
let vue = require("vue");
|
|
4
|
+
let _flareapp_core_util = require("@flareapp/core/util");
|
|
3
5
|
|
|
4
6
|
//#region src/resolveFlare.ts
|
|
5
|
-
|
|
6
|
-
function isDevMode() {
|
|
7
|
-
try {
|
|
8
|
-
return process.env.NODE_ENV !== "production";
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function registerDefaultFlare(provider) {
|
|
14
|
-
if (typeof window !== "undefined" && window.__flare) {
|
|
15
|
-
const message = "[flare] @flareapp/vue (web root) was imported in a renderer where the Electron bridge is present, pulling the keyed @flareapp/js singleton into the renderer. Import @flareapp/vue/inject and pass the @flareapp/electron/renderer instance instead.";
|
|
16
|
-
if (isDevMode()) throw new Error(message);
|
|
17
|
-
console.warn(message);
|
|
18
|
-
}
|
|
19
|
-
defaultProvider = provider;
|
|
20
|
-
}
|
|
21
|
-
function resolveFlare(explicit) {
|
|
22
|
-
if (explicit) return explicit;
|
|
23
|
-
if (defaultProvider) return defaultProvider();
|
|
24
|
-
throw new Error("[flare] No Flare instance available. Pass `flare` (e.g. from @flareapp/electron/renderer), or import @flareapp/vue (the package root) to use the @flareapp/js default singleton.");
|
|
25
|
-
}
|
|
7
|
+
const { registerDefaultFlare, resolveFlare } = (0, _flareapp_js_browser.createFlareResolver)({ packageName: "@flareapp/vue" });
|
|
26
8
|
|
|
27
9
|
//#endregion
|
|
28
10
|
//#region src/constants.ts
|
|
11
|
+
/** Injected at build time via tsdown --env.PACKAGE_VERSION (reads package.json version). */
|
|
29
12
|
const PACKAGE_VERSION = typeof process !== "undefined" && typeof process.env?.PACKAGE_VERSION !== "undefined" ? process.env.PACKAGE_VERSION : "?";
|
|
30
13
|
const MAX_HIERARCHY_DEPTH = 50;
|
|
31
14
|
const DEFAULT_PROPS_DENYLIST = /password|passwd|pwd|token|secret|authorization|\bauth\b|bearer|oauth|credentials?|cookie|api[-_]?key|private[-_]?key|session|csrf|xsrf|\bpin\b|\bssn\b|card[-_]?number|\bcvv\b/i;
|
|
@@ -122,51 +105,19 @@ function buildComponentHierarchy(instance) {
|
|
|
122
105
|
|
|
123
106
|
//#endregion
|
|
124
107
|
//#region src/serializeProps.ts
|
|
108
|
+
/**
|
|
109
|
+
* JSON-safe, redacted, size-bounded copy of a Vue component's props for the report payload. Delegates
|
|
110
|
+
* to core safeClone (display mode) so the cycle / BigInt / throwing-getter safety is shared with core.
|
|
111
|
+
*/
|
|
125
112
|
function serializeProps(value, maxDepth, denylist = DEFAULT_PROPS_DENYLIST) {
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (type === "string") return truncateString(value);
|
|
135
|
-
if (type !== "object") return value;
|
|
136
|
-
if (seen.has(value)) return "[Circular]";
|
|
137
|
-
if (Array.isArray(value)) {
|
|
138
|
-
if (depth > maxDepth) return "[Array]";
|
|
139
|
-
seen.add(value);
|
|
140
|
-
const out = (value.length > MAX_PROP_ARRAY_LENGTH ? value.slice(0, MAX_PROP_ARRAY_LENGTH) : value).map((item) => serialize(item, depth + 1, maxDepth, seen, denylist));
|
|
141
|
-
if (value.length > MAX_PROP_ARRAY_LENGTH) out.push(`[… ${value.length - MAX_PROP_ARRAY_LENGTH} more items]`);
|
|
142
|
-
seen.delete(value);
|
|
143
|
-
return out;
|
|
144
|
-
}
|
|
145
|
-
if (!isPlainObject(value)) return "[Object]";
|
|
146
|
-
if (depth > maxDepth) return "[Object]";
|
|
147
|
-
seen.add(value);
|
|
148
|
-
const out = {};
|
|
149
|
-
const keys = Object.keys(value);
|
|
150
|
-
const limitedKeys = keys.length > MAX_PROP_OBJECT_KEYS ? keys.slice(0, MAX_PROP_OBJECT_KEYS) : keys;
|
|
151
|
-
for (const key of limitedKeys) {
|
|
152
|
-
if (denylist.test(key)) {
|
|
153
|
-
out[key] = "[redacted]";
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
out[key] = serialize(value[key], depth + 1, maxDepth, seen, denylist);
|
|
157
|
-
}
|
|
158
|
-
if (keys.length > MAX_PROP_OBJECT_KEYS) out["…"] = `[${keys.length - MAX_PROP_OBJECT_KEYS} more keys]`;
|
|
159
|
-
seen.delete(value);
|
|
160
|
-
return out;
|
|
161
|
-
}
|
|
162
|
-
function truncateString(value) {
|
|
163
|
-
if (value.length <= MAX_PROP_STRING_LENGTH) return value;
|
|
164
|
-
return `${value.slice(0, MAX_PROP_STRING_LENGTH)}…[truncated ${value.length - MAX_PROP_STRING_LENGTH} chars]`;
|
|
165
|
-
}
|
|
166
|
-
function isPlainObject(value) {
|
|
167
|
-
if (value === null || typeof value !== "object") return false;
|
|
168
|
-
const prototype = Object.getPrototypeOf(value);
|
|
169
|
-
return prototype === null || prototype === Object.prototype;
|
|
113
|
+
return (0, _flareapp_core.safeClone)(value, {
|
|
114
|
+
mode: "display",
|
|
115
|
+
maxDepth,
|
|
116
|
+
arrayCap: MAX_PROP_ARRAY_LENGTH,
|
|
117
|
+
objectKeyCap: MAX_PROP_OBJECT_KEYS,
|
|
118
|
+
stringCap: MAX_PROP_STRING_LENGTH,
|
|
119
|
+
denylist
|
|
120
|
+
});
|
|
170
121
|
}
|
|
171
122
|
|
|
172
123
|
//#endregion
|
|
@@ -189,6 +140,10 @@ function buildComponentHierarchyFrames(instance, options) {
|
|
|
189
140
|
|
|
190
141
|
//#endregion
|
|
191
142
|
//#region src/getErrorOrigin.ts
|
|
143
|
+
/**
|
|
144
|
+
* Vue's `info` is a human-readable string in dev (e.g. "render function") but a numeric/short code
|
|
145
|
+
* in production. INFO_TO_ORIGIN maps both forms to a stable origin category.
|
|
146
|
+
*/
|
|
192
147
|
function getErrorOrigin(info) {
|
|
193
148
|
return INFO_TO_ORIGIN[info] ?? "unknown";
|
|
194
149
|
}
|
|
@@ -196,6 +151,29 @@ function getErrorOrigin(info) {
|
|
|
196
151
|
//#endregion
|
|
197
152
|
//#region src/getRouteContext.ts
|
|
198
153
|
const ROUTE_PARAMS_DEPTH = 2;
|
|
154
|
+
/** vue-router allows a symbol route name; neither shape survives JSON, so convert both to a string. */
|
|
155
|
+
function routeNameOf(value) {
|
|
156
|
+
if (typeof value === "string") return value;
|
|
157
|
+
if (typeof value === "symbol") return value.toString();
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function matchedNames(matched) {
|
|
161
|
+
if (!Array.isArray(matched)) return [];
|
|
162
|
+
return matched.map((record) => {
|
|
163
|
+
if (!record || typeof record !== "object") return "unknown";
|
|
164
|
+
return routeNameOf(record.name) ?? "unknown";
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function recordOrEmpty(value) {
|
|
168
|
+
if (!value || typeof value !== "object") return {};
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `router` is typed `unknown` because vue-router is an optional peer (importing it would force every
|
|
173
|
+
* consumer to install it, and the runtime shape may differ across v4.x patches). The chained
|
|
174
|
+
* type-guards read defensively so a missing or shimmed router yields `null` rather than throwing
|
|
175
|
+
* inside an error handler.
|
|
176
|
+
*/
|
|
199
177
|
function getRouteContext(router, options = {}) {
|
|
200
178
|
if (!router || typeof router !== "object" || !("currentRoute" in router)) return null;
|
|
201
179
|
const currentRouteRef = router.currentRoute;
|
|
@@ -203,44 +181,180 @@ function getRouteContext(router, options = {}) {
|
|
|
203
181
|
const route = currentRouteRef.value;
|
|
204
182
|
if (!route || typeof route !== "object") return null;
|
|
205
183
|
const r = route;
|
|
206
|
-
const name = r.name;
|
|
207
184
|
const denylist = options.denylist ?? DEFAULT_PROPS_DENYLIST;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
185
|
+
const redactedParams = (0, _flareapp_core.redactObjectValues)(recordOrEmpty(r.params), denylist);
|
|
186
|
+
const redactedQuery = (0, _flareapp_core.redactObjectValues)(recordOrEmpty(r.query), denylist);
|
|
210
187
|
return {
|
|
211
|
-
name:
|
|
188
|
+
name: routeNameOf(r.name),
|
|
212
189
|
path: typeof r.path === "string" ? r.path : "",
|
|
213
190
|
fullPath: typeof r.fullPath === "string" ? (0, _flareapp_core.redactUrlQuery)(r.fullPath, denylist) : "",
|
|
214
|
-
params: serializeProps(
|
|
215
|
-
query: serializeProps(
|
|
191
|
+
params: serializeProps(redactedParams, ROUTE_PARAMS_DEPTH, denylist),
|
|
192
|
+
query: serializeProps(redactedQuery, ROUTE_PARAMS_DEPTH, denylist),
|
|
216
193
|
hash: typeof r.hash === "string" ? r.hash : "",
|
|
217
|
-
matched:
|
|
218
|
-
if (!record || typeof record !== "object") return "unknown";
|
|
219
|
-
const n = record.name;
|
|
220
|
-
return typeof n === "string" ? n : typeof n === "symbol" ? n.toString() : "unknown";
|
|
221
|
-
}) : []
|
|
194
|
+
matched: matchedNames(r.matched)
|
|
222
195
|
};
|
|
223
196
|
}
|
|
224
197
|
|
|
225
198
|
//#endregion
|
|
226
199
|
//#region src/identify.ts
|
|
227
|
-
const
|
|
228
|
-
|
|
200
|
+
const tagger = (0, _flareapp_core.createIdentityTagger)({
|
|
201
|
+
sdkName: "@flareapp/vue",
|
|
202
|
+
sdkVersion: PACKAGE_VERSION,
|
|
203
|
+
frameworkName: _flareapp_core.FrameworkName.Vue
|
|
204
|
+
});
|
|
205
|
+
/** Web path: SDK identity only; the framework version (app.version) is only known at install time. */
|
|
229
206
|
function registerVueSdkInfo(flare) {
|
|
230
|
-
|
|
231
|
-
sdkTagged.add(flare);
|
|
232
|
-
flare.setSdkInfo({
|
|
233
|
-
name: "@flareapp/vue",
|
|
234
|
-
version: PACKAGE_VERSION
|
|
235
|
-
});
|
|
207
|
+
tagger.registerSdkIdentity(flare);
|
|
236
208
|
}
|
|
209
|
+
/** Both paths tag the framework; never touches sdkInfo (would clobber an injected SDK name). */
|
|
237
210
|
function tagVueFramework(flare, appVersion) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
211
|
+
tagger.tagFramework(flare, appVersion);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/profileVueComponents.ts
|
|
216
|
+
const PROFILE = Symbol("flareComponentProfile");
|
|
217
|
+
/** Only matched components store a marker, so unmatched ones, and functional components, which get
|
|
218
|
+
* no lifecycle hooks at all, need no code of their own. */
|
|
219
|
+
function nearestMarker(instance) {
|
|
220
|
+
for (let node = instance.parent; node; node = node.parent) {
|
|
221
|
+
const state = node[PROFILE];
|
|
222
|
+
if (state) return state.marker;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Record one `browser_component` span per matched component mount. `beforeMount` reserves the span id
|
|
228
|
+
* and captures the start; `mounted` records it. Vue runs `beforeMount` top-down and `mounted` bottom-up,
|
|
229
|
+
* so a parent's span encloses its SYNCHRONOUS descendants in time. Async components and anything under
|
|
230
|
+
* `<Suspense>` are outside that contract: their span can start after the parent's ended, or be dropped
|
|
231
|
+
* entirely when the root closed in the meantime. Nesting by parent id holds while the trace is the
|
|
232
|
+
* same; a trace change re-homes a descendant to the live root instead of its dead-trace ancestor.
|
|
233
|
+
*/
|
|
234
|
+
function createComponentProfilerMixin(matches) {
|
|
235
|
+
return {
|
|
236
|
+
beforeMount() {
|
|
237
|
+
try {
|
|
238
|
+
const live = (0, _flareapp_js_browser.activeComponentRoot)();
|
|
239
|
+
if (!live) return;
|
|
240
|
+
const name = getComponentName(this);
|
|
241
|
+
if (!matches(name)) return;
|
|
242
|
+
const internal = this.$;
|
|
243
|
+
const parent = (0, _flareapp_js_browser.resolveComponentParent)(nearestMarker(internal), live) ?? live;
|
|
244
|
+
const spanId = (0, _flareapp_js_browser.reserveSpanId)(parent.traceId);
|
|
245
|
+
if (!spanId) return;
|
|
246
|
+
internal[PROFILE] = {
|
|
247
|
+
marker: {
|
|
248
|
+
traceId: parent.traceId,
|
|
249
|
+
parentSpanId: spanId
|
|
250
|
+
},
|
|
251
|
+
pending: {
|
|
252
|
+
name,
|
|
253
|
+
spanId,
|
|
254
|
+
startNano: (0, _flareapp_js_browser.nowNano)(),
|
|
255
|
+
parent
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
} catch {}
|
|
259
|
+
},
|
|
260
|
+
mounted() {
|
|
261
|
+
try {
|
|
262
|
+
const state = this.$[PROFILE];
|
|
263
|
+
const pending = state?.pending;
|
|
264
|
+
if (!state || !pending) return;
|
|
265
|
+
state.pending = null;
|
|
266
|
+
(0, _flareapp_js_browser.recordComponentSpan)({
|
|
267
|
+
name: pending.name,
|
|
268
|
+
spanId: pending.spanId,
|
|
269
|
+
parent: pending.parent,
|
|
270
|
+
startTimeUnixNano: pending.startNano,
|
|
271
|
+
endTimeUnixNano: (0, _flareapp_js_browser.nowNano)()
|
|
272
|
+
});
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/traceVueRouter.ts
|
|
280
|
+
const NAVIGATION_CANCELLED = 8;
|
|
281
|
+
/** Internal, wired through `flareVue({ router })`. Opens a held navigation root per route change,
|
|
282
|
+
* settled when the navigation confirms. */
|
|
283
|
+
function traceVueRouter(router) {
|
|
284
|
+
if (!isVueRouter(router)) return () => {};
|
|
285
|
+
return (0, _flareapp_js_browser.instrumentOnce)(router, (track) => install(router, track));
|
|
286
|
+
}
|
|
287
|
+
/** Guards only what the integration calls unconditionally; `resolve` and `onError` stay optional. */
|
|
288
|
+
function isVueRouter(router) {
|
|
289
|
+
if (typeof router !== "object" || router === null) return false;
|
|
290
|
+
return "beforeEach" in router && typeof router.beforeEach === "function" && "afterEach" in router && typeof router.afterEach === "function";
|
|
291
|
+
}
|
|
292
|
+
function install(router, track) {
|
|
293
|
+
const nav = (0, _flareapp_js_browser.registerNavigationSource)();
|
|
294
|
+
track(() => nav.unregister());
|
|
295
|
+
function routeNameFor(loc) {
|
|
296
|
+
return (0, _flareapp_js_browser.routeName)(() => loc.matched?.[loc.matched.length - 1]?.path, loc.path, hrefOf(loc));
|
|
297
|
+
}
|
|
298
|
+
function hrefOf(loc) {
|
|
299
|
+
const path = loc.fullPath ?? loc.path;
|
|
300
|
+
if (!path) return;
|
|
301
|
+
return (0, _flareapp_js_browser.resolveHref)(() => router.resolve?.(path)?.href, path);
|
|
302
|
+
}
|
|
303
|
+
function isInitial(from) {
|
|
304
|
+
return !from || !from.matched || from.matched.length === 0;
|
|
305
|
+
}
|
|
306
|
+
let sawInitial = false;
|
|
307
|
+
let inFlight = false;
|
|
308
|
+
try {
|
|
309
|
+
const current = router.currentRoute?.value;
|
|
310
|
+
if (current && current.matched && current.matched.length > 0) {
|
|
311
|
+
nav.setActiveRouteName(routeNameFor(current));
|
|
312
|
+
sawInitial = true;
|
|
313
|
+
}
|
|
314
|
+
} catch {}
|
|
315
|
+
track(router.beforeEach((0, _flareapp_js_browser.insulate)((to, from) => {
|
|
316
|
+
if (!sawInitial && isInitial(from)) {
|
|
317
|
+
nav.setActiveRouteName(routeNameFor(to));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (to.fullPath && from?.fullPath && to.fullPath === from.fullPath) return;
|
|
321
|
+
if (!inFlight) {
|
|
322
|
+
inFlight = true;
|
|
323
|
+
nav.startNavigation({
|
|
324
|
+
path: to.path,
|
|
325
|
+
url: hrefOf(to),
|
|
326
|
+
hold: true
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
nav.setActiveRouteName(routeNameFor(to));
|
|
330
|
+
})));
|
|
331
|
+
track(router.afterEach((0, _flareapp_js_browser.insulate)((to, from, failure) => {
|
|
332
|
+
if (!sawInitial && isInitial(from)) {
|
|
333
|
+
if (!failure) {
|
|
334
|
+
sawInitial = true;
|
|
335
|
+
nav.setActiveRouteName(routeNameFor(to));
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!inFlight) return;
|
|
340
|
+
if (!failure) {
|
|
341
|
+
inFlight = false;
|
|
342
|
+
nav.settleNavigation(routeNameFor(to));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (failure.type === NAVIGATION_CANCELLED) return;
|
|
346
|
+
inFlight = false;
|
|
347
|
+
nav.settleNavigation(routeNameFor(from));
|
|
348
|
+
})));
|
|
349
|
+
if (typeof router.onError === "function") track(router.onError((0, _flareapp_js_browser.insulate)(() => {
|
|
350
|
+
if (!inFlight) return;
|
|
351
|
+
inFlight = false;
|
|
352
|
+
const current = router.currentRoute?.value;
|
|
353
|
+
nav.settleNavigation(current ? routeNameFor(current) : {
|
|
354
|
+
name: "",
|
|
355
|
+
source: "url"
|
|
356
|
+
});
|
|
357
|
+
})));
|
|
244
358
|
}
|
|
245
359
|
|
|
246
360
|
//#endregion
|
|
@@ -255,7 +369,7 @@ function vueContextToAttributes(context) {
|
|
|
255
369
|
};
|
|
256
370
|
if (context.vue.componentProps) vue.componentProps = context.vue.componentProps;
|
|
257
371
|
if (context.vue.route) vue.route = context.vue.route;
|
|
258
|
-
return
|
|
372
|
+
return (0, _flareapp_core.toCustomContext)("vue", vue);
|
|
259
373
|
}
|
|
260
374
|
function vueWarningContextToAttributes(context) {
|
|
261
375
|
const vue = {
|
|
@@ -265,7 +379,7 @@ function vueWarningContextToAttributes(context) {
|
|
|
265
379
|
componentTrace: context.vue.componentTrace
|
|
266
380
|
};
|
|
267
381
|
if (context.vue.route) vue.route = context.vue.route;
|
|
268
|
-
return
|
|
382
|
+
return (0, _flareapp_core.toCustomContext)("vue", vue);
|
|
269
383
|
}
|
|
270
384
|
const installedApps = /* @__PURE__ */ new WeakSet();
|
|
271
385
|
const flareVue = (app, options) => {
|
|
@@ -339,6 +453,15 @@ const flareVue = (app, options) => {
|
|
|
339
453
|
if (typeof initialWarnHandler === "function") initialWarnHandler(msg, instance, trace);
|
|
340
454
|
};
|
|
341
455
|
}
|
|
456
|
+
if (options?.router) try {
|
|
457
|
+
const stopRouterTracing = traceVueRouter(options.router);
|
|
458
|
+
if (typeof app.onUnmount === "function") app.onUnmount(stopRouterTracing);
|
|
459
|
+
} catch {}
|
|
460
|
+
const profile = options?.profileComponents;
|
|
461
|
+
if (!(profile === true || Array.isArray(profile) && profile.length > 0)) return;
|
|
462
|
+
try {
|
|
463
|
+
app.mixin(createComponentProfilerMixin((0, _flareapp_core_util.createComponentMatcher)(profile)));
|
|
464
|
+
} catch {}
|
|
342
465
|
};
|
|
343
466
|
|
|
344
467
|
//#endregion
|
|
@@ -1,31 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { activeComponentRoot, createFlareResolver, instrumentOnce, insulate, nowNano, recordComponentSpan, registerNavigationSource, reserveSpanId, resolveComponentParent, resolveHref, routeName } from "@flareapp/js/browser";
|
|
2
|
+
import { FrameworkName, convertToError, createIdentityTagger, redactObjectValues, redactUrlQuery, resolveDenylist, safeClone, toCustomContext } from "@flareapp/core";
|
|
2
3
|
import { defineComponent, getCurrentInstance, onErrorCaptured, ref, watch } from "vue";
|
|
4
|
+
import { createComponentMatcher } from "@flareapp/core/util";
|
|
3
5
|
|
|
4
6
|
//#region src/resolveFlare.ts
|
|
5
|
-
|
|
6
|
-
function isDevMode() {
|
|
7
|
-
try {
|
|
8
|
-
return process.env.NODE_ENV !== "production";
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function registerDefaultFlare(provider) {
|
|
14
|
-
if (typeof window !== "undefined" && window.__flare) {
|
|
15
|
-
const message = "[flare] @flareapp/vue (web root) was imported in a renderer where the Electron bridge is present, pulling the keyed @flareapp/js singleton into the renderer. Import @flareapp/vue/inject and pass the @flareapp/electron/renderer instance instead.";
|
|
16
|
-
if (isDevMode()) throw new Error(message);
|
|
17
|
-
console.warn(message);
|
|
18
|
-
}
|
|
19
|
-
defaultProvider = provider;
|
|
20
|
-
}
|
|
21
|
-
function resolveFlare(explicit) {
|
|
22
|
-
if (explicit) return explicit;
|
|
23
|
-
if (defaultProvider) return defaultProvider();
|
|
24
|
-
throw new Error("[flare] No Flare instance available. Pass `flare` (e.g. from @flareapp/electron/renderer), or import @flareapp/vue (the package root) to use the @flareapp/js default singleton.");
|
|
25
|
-
}
|
|
7
|
+
const { registerDefaultFlare, resolveFlare } = createFlareResolver({ packageName: "@flareapp/vue" });
|
|
26
8
|
|
|
27
9
|
//#endregion
|
|
28
10
|
//#region src/constants.ts
|
|
11
|
+
/** Injected at build time via tsdown --env.PACKAGE_VERSION (reads package.json version). */
|
|
29
12
|
const PACKAGE_VERSION = typeof process !== "undefined" && typeof process.env?.PACKAGE_VERSION !== "undefined" ? process.env.PACKAGE_VERSION : "?";
|
|
30
13
|
const MAX_HIERARCHY_DEPTH = 50;
|
|
31
14
|
const DEFAULT_PROPS_DENYLIST = /password|passwd|pwd|token|secret|authorization|\bauth\b|bearer|oauth|credentials?|cookie|api[-_]?key|private[-_]?key|session|csrf|xsrf|\bpin\b|\bssn\b|card[-_]?number|\bcvv\b/i;
|
|
@@ -122,51 +105,19 @@ function buildComponentHierarchy(instance) {
|
|
|
122
105
|
|
|
123
106
|
//#endregion
|
|
124
107
|
//#region src/serializeProps.ts
|
|
108
|
+
/**
|
|
109
|
+
* JSON-safe, redacted, size-bounded copy of a Vue component's props for the report payload. Delegates
|
|
110
|
+
* to core safeClone (display mode) so the cycle / BigInt / throwing-getter safety is shared with core.
|
|
111
|
+
*/
|
|
125
112
|
function serializeProps(value, maxDepth, denylist = DEFAULT_PROPS_DENYLIST) {
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (type === "string") return truncateString(value);
|
|
135
|
-
if (type !== "object") return value;
|
|
136
|
-
if (seen.has(value)) return "[Circular]";
|
|
137
|
-
if (Array.isArray(value)) {
|
|
138
|
-
if (depth > maxDepth) return "[Array]";
|
|
139
|
-
seen.add(value);
|
|
140
|
-
const out = (value.length > MAX_PROP_ARRAY_LENGTH ? value.slice(0, MAX_PROP_ARRAY_LENGTH) : value).map((item) => serialize(item, depth + 1, maxDepth, seen, denylist));
|
|
141
|
-
if (value.length > MAX_PROP_ARRAY_LENGTH) out.push(`[… ${value.length - MAX_PROP_ARRAY_LENGTH} more items]`);
|
|
142
|
-
seen.delete(value);
|
|
143
|
-
return out;
|
|
144
|
-
}
|
|
145
|
-
if (!isPlainObject(value)) return "[Object]";
|
|
146
|
-
if (depth > maxDepth) return "[Object]";
|
|
147
|
-
seen.add(value);
|
|
148
|
-
const out = {};
|
|
149
|
-
const keys = Object.keys(value);
|
|
150
|
-
const limitedKeys = keys.length > MAX_PROP_OBJECT_KEYS ? keys.slice(0, MAX_PROP_OBJECT_KEYS) : keys;
|
|
151
|
-
for (const key of limitedKeys) {
|
|
152
|
-
if (denylist.test(key)) {
|
|
153
|
-
out[key] = "[redacted]";
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
out[key] = serialize(value[key], depth + 1, maxDepth, seen, denylist);
|
|
157
|
-
}
|
|
158
|
-
if (keys.length > MAX_PROP_OBJECT_KEYS) out["…"] = `[${keys.length - MAX_PROP_OBJECT_KEYS} more keys]`;
|
|
159
|
-
seen.delete(value);
|
|
160
|
-
return out;
|
|
161
|
-
}
|
|
162
|
-
function truncateString(value) {
|
|
163
|
-
if (value.length <= MAX_PROP_STRING_LENGTH) return value;
|
|
164
|
-
return `${value.slice(0, MAX_PROP_STRING_LENGTH)}…[truncated ${value.length - MAX_PROP_STRING_LENGTH} chars]`;
|
|
165
|
-
}
|
|
166
|
-
function isPlainObject(value) {
|
|
167
|
-
if (value === null || typeof value !== "object") return false;
|
|
168
|
-
const prototype = Object.getPrototypeOf(value);
|
|
169
|
-
return prototype === null || prototype === Object.prototype;
|
|
113
|
+
return safeClone(value, {
|
|
114
|
+
mode: "display",
|
|
115
|
+
maxDepth,
|
|
116
|
+
arrayCap: MAX_PROP_ARRAY_LENGTH,
|
|
117
|
+
objectKeyCap: MAX_PROP_OBJECT_KEYS,
|
|
118
|
+
stringCap: MAX_PROP_STRING_LENGTH,
|
|
119
|
+
denylist
|
|
120
|
+
});
|
|
170
121
|
}
|
|
171
122
|
|
|
172
123
|
//#endregion
|
|
@@ -189,6 +140,10 @@ function buildComponentHierarchyFrames(instance, options) {
|
|
|
189
140
|
|
|
190
141
|
//#endregion
|
|
191
142
|
//#region src/getErrorOrigin.ts
|
|
143
|
+
/**
|
|
144
|
+
* Vue's `info` is a human-readable string in dev (e.g. "render function") but a numeric/short code
|
|
145
|
+
* in production. INFO_TO_ORIGIN maps both forms to a stable origin category.
|
|
146
|
+
*/
|
|
192
147
|
function getErrorOrigin(info) {
|
|
193
148
|
return INFO_TO_ORIGIN[info] ?? "unknown";
|
|
194
149
|
}
|
|
@@ -196,6 +151,29 @@ function getErrorOrigin(info) {
|
|
|
196
151
|
//#endregion
|
|
197
152
|
//#region src/getRouteContext.ts
|
|
198
153
|
const ROUTE_PARAMS_DEPTH = 2;
|
|
154
|
+
/** vue-router allows a symbol route name; neither shape survives JSON, so convert both to a string. */
|
|
155
|
+
function routeNameOf(value) {
|
|
156
|
+
if (typeof value === "string") return value;
|
|
157
|
+
if (typeof value === "symbol") return value.toString();
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function matchedNames(matched) {
|
|
161
|
+
if (!Array.isArray(matched)) return [];
|
|
162
|
+
return matched.map((record) => {
|
|
163
|
+
if (!record || typeof record !== "object") return "unknown";
|
|
164
|
+
return routeNameOf(record.name) ?? "unknown";
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function recordOrEmpty(value) {
|
|
168
|
+
if (!value || typeof value !== "object") return {};
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `router` is typed `unknown` because vue-router is an optional peer (importing it would force every
|
|
173
|
+
* consumer to install it, and the runtime shape may differ across v4.x patches). The chained
|
|
174
|
+
* type-guards read defensively so a missing or shimmed router yields `null` rather than throwing
|
|
175
|
+
* inside an error handler.
|
|
176
|
+
*/
|
|
199
177
|
function getRouteContext(router, options = {}) {
|
|
200
178
|
if (!router || typeof router !== "object" || !("currentRoute" in router)) return null;
|
|
201
179
|
const currentRouteRef = router.currentRoute;
|
|
@@ -203,44 +181,180 @@ function getRouteContext(router, options = {}) {
|
|
|
203
181
|
const route = currentRouteRef.value;
|
|
204
182
|
if (!route || typeof route !== "object") return null;
|
|
205
183
|
const r = route;
|
|
206
|
-
const name = r.name;
|
|
207
184
|
const denylist = options.denylist ?? DEFAULT_PROPS_DENYLIST;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
185
|
+
const redactedParams = redactObjectValues(recordOrEmpty(r.params), denylist);
|
|
186
|
+
const redactedQuery = redactObjectValues(recordOrEmpty(r.query), denylist);
|
|
210
187
|
return {
|
|
211
|
-
name:
|
|
188
|
+
name: routeNameOf(r.name),
|
|
212
189
|
path: typeof r.path === "string" ? r.path : "",
|
|
213
190
|
fullPath: typeof r.fullPath === "string" ? redactUrlQuery(r.fullPath, denylist) : "",
|
|
214
|
-
params: serializeProps(
|
|
215
|
-
query: serializeProps(
|
|
191
|
+
params: serializeProps(redactedParams, ROUTE_PARAMS_DEPTH, denylist),
|
|
192
|
+
query: serializeProps(redactedQuery, ROUTE_PARAMS_DEPTH, denylist),
|
|
216
193
|
hash: typeof r.hash === "string" ? r.hash : "",
|
|
217
|
-
matched:
|
|
218
|
-
if (!record || typeof record !== "object") return "unknown";
|
|
219
|
-
const n = record.name;
|
|
220
|
-
return typeof n === "string" ? n : typeof n === "symbol" ? n.toString() : "unknown";
|
|
221
|
-
}) : []
|
|
194
|
+
matched: matchedNames(r.matched)
|
|
222
195
|
};
|
|
223
196
|
}
|
|
224
197
|
|
|
225
198
|
//#endregion
|
|
226
199
|
//#region src/identify.ts
|
|
227
|
-
const
|
|
228
|
-
|
|
200
|
+
const tagger = createIdentityTagger({
|
|
201
|
+
sdkName: "@flareapp/vue",
|
|
202
|
+
sdkVersion: PACKAGE_VERSION,
|
|
203
|
+
frameworkName: FrameworkName.Vue
|
|
204
|
+
});
|
|
205
|
+
/** Web path: SDK identity only; the framework version (app.version) is only known at install time. */
|
|
229
206
|
function registerVueSdkInfo(flare) {
|
|
230
|
-
|
|
231
|
-
sdkTagged.add(flare);
|
|
232
|
-
flare.setSdkInfo({
|
|
233
|
-
name: "@flareapp/vue",
|
|
234
|
-
version: PACKAGE_VERSION
|
|
235
|
-
});
|
|
207
|
+
tagger.registerSdkIdentity(flare);
|
|
236
208
|
}
|
|
209
|
+
/** Both paths tag the framework; never touches sdkInfo (would clobber an injected SDK name). */
|
|
237
210
|
function tagVueFramework(flare, appVersion) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
211
|
+
tagger.tagFramework(flare, appVersion);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/profileVueComponents.ts
|
|
216
|
+
const PROFILE = Symbol("flareComponentProfile");
|
|
217
|
+
/** Only matched components store a marker, so unmatched ones, and functional components, which get
|
|
218
|
+
* no lifecycle hooks at all, need no code of their own. */
|
|
219
|
+
function nearestMarker(instance) {
|
|
220
|
+
for (let node = instance.parent; node; node = node.parent) {
|
|
221
|
+
const state = node[PROFILE];
|
|
222
|
+
if (state) return state.marker;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Record one `browser_component` span per matched component mount. `beforeMount` reserves the span id
|
|
228
|
+
* and captures the start; `mounted` records it. Vue runs `beforeMount` top-down and `mounted` bottom-up,
|
|
229
|
+
* so a parent's span encloses its SYNCHRONOUS descendants in time. Async components and anything under
|
|
230
|
+
* `<Suspense>` are outside that contract: their span can start after the parent's ended, or be dropped
|
|
231
|
+
* entirely when the root closed in the meantime. Nesting by parent id holds while the trace is the
|
|
232
|
+
* same; a trace change re-homes a descendant to the live root instead of its dead-trace ancestor.
|
|
233
|
+
*/
|
|
234
|
+
function createComponentProfilerMixin(matches) {
|
|
235
|
+
return {
|
|
236
|
+
beforeMount() {
|
|
237
|
+
try {
|
|
238
|
+
const live = activeComponentRoot();
|
|
239
|
+
if (!live) return;
|
|
240
|
+
const name = getComponentName(this);
|
|
241
|
+
if (!matches(name)) return;
|
|
242
|
+
const internal = this.$;
|
|
243
|
+
const parent = resolveComponentParent(nearestMarker(internal), live) ?? live;
|
|
244
|
+
const spanId = reserveSpanId(parent.traceId);
|
|
245
|
+
if (!spanId) return;
|
|
246
|
+
internal[PROFILE] = {
|
|
247
|
+
marker: {
|
|
248
|
+
traceId: parent.traceId,
|
|
249
|
+
parentSpanId: spanId
|
|
250
|
+
},
|
|
251
|
+
pending: {
|
|
252
|
+
name,
|
|
253
|
+
spanId,
|
|
254
|
+
startNano: nowNano(),
|
|
255
|
+
parent
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
} catch {}
|
|
259
|
+
},
|
|
260
|
+
mounted() {
|
|
261
|
+
try {
|
|
262
|
+
const state = this.$[PROFILE];
|
|
263
|
+
const pending = state?.pending;
|
|
264
|
+
if (!state || !pending) return;
|
|
265
|
+
state.pending = null;
|
|
266
|
+
recordComponentSpan({
|
|
267
|
+
name: pending.name,
|
|
268
|
+
spanId: pending.spanId,
|
|
269
|
+
parent: pending.parent,
|
|
270
|
+
startTimeUnixNano: pending.startNano,
|
|
271
|
+
endTimeUnixNano: nowNano()
|
|
272
|
+
});
|
|
273
|
+
} catch {}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/traceVueRouter.ts
|
|
280
|
+
const NAVIGATION_CANCELLED = 8;
|
|
281
|
+
/** Internal, wired through `flareVue({ router })`. Opens a held navigation root per route change,
|
|
282
|
+
* settled when the navigation confirms. */
|
|
283
|
+
function traceVueRouter(router) {
|
|
284
|
+
if (!isVueRouter(router)) return () => {};
|
|
285
|
+
return instrumentOnce(router, (track) => install(router, track));
|
|
286
|
+
}
|
|
287
|
+
/** Guards only what the integration calls unconditionally; `resolve` and `onError` stay optional. */
|
|
288
|
+
function isVueRouter(router) {
|
|
289
|
+
if (typeof router !== "object" || router === null) return false;
|
|
290
|
+
return "beforeEach" in router && typeof router.beforeEach === "function" && "afterEach" in router && typeof router.afterEach === "function";
|
|
291
|
+
}
|
|
292
|
+
function install(router, track) {
|
|
293
|
+
const nav = registerNavigationSource();
|
|
294
|
+
track(() => nav.unregister());
|
|
295
|
+
function routeNameFor(loc) {
|
|
296
|
+
return routeName(() => loc.matched?.[loc.matched.length - 1]?.path, loc.path, hrefOf(loc));
|
|
297
|
+
}
|
|
298
|
+
function hrefOf(loc) {
|
|
299
|
+
const path = loc.fullPath ?? loc.path;
|
|
300
|
+
if (!path) return;
|
|
301
|
+
return resolveHref(() => router.resolve?.(path)?.href, path);
|
|
302
|
+
}
|
|
303
|
+
function isInitial(from) {
|
|
304
|
+
return !from || !from.matched || from.matched.length === 0;
|
|
305
|
+
}
|
|
306
|
+
let sawInitial = false;
|
|
307
|
+
let inFlight = false;
|
|
308
|
+
try {
|
|
309
|
+
const current = router.currentRoute?.value;
|
|
310
|
+
if (current && current.matched && current.matched.length > 0) {
|
|
311
|
+
nav.setActiveRouteName(routeNameFor(current));
|
|
312
|
+
sawInitial = true;
|
|
313
|
+
}
|
|
314
|
+
} catch {}
|
|
315
|
+
track(router.beforeEach(insulate((to, from) => {
|
|
316
|
+
if (!sawInitial && isInitial(from)) {
|
|
317
|
+
nav.setActiveRouteName(routeNameFor(to));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (to.fullPath && from?.fullPath && to.fullPath === from.fullPath) return;
|
|
321
|
+
if (!inFlight) {
|
|
322
|
+
inFlight = true;
|
|
323
|
+
nav.startNavigation({
|
|
324
|
+
path: to.path,
|
|
325
|
+
url: hrefOf(to),
|
|
326
|
+
hold: true
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
nav.setActiveRouteName(routeNameFor(to));
|
|
330
|
+
})));
|
|
331
|
+
track(router.afterEach(insulate((to, from, failure) => {
|
|
332
|
+
if (!sawInitial && isInitial(from)) {
|
|
333
|
+
if (!failure) {
|
|
334
|
+
sawInitial = true;
|
|
335
|
+
nav.setActiveRouteName(routeNameFor(to));
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!inFlight) return;
|
|
340
|
+
if (!failure) {
|
|
341
|
+
inFlight = false;
|
|
342
|
+
nav.settleNavigation(routeNameFor(to));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (failure.type === NAVIGATION_CANCELLED) return;
|
|
346
|
+
inFlight = false;
|
|
347
|
+
nav.settleNavigation(routeNameFor(from));
|
|
348
|
+
})));
|
|
349
|
+
if (typeof router.onError === "function") track(router.onError(insulate(() => {
|
|
350
|
+
if (!inFlight) return;
|
|
351
|
+
inFlight = false;
|
|
352
|
+
const current = router.currentRoute?.value;
|
|
353
|
+
nav.settleNavigation(current ? routeNameFor(current) : {
|
|
354
|
+
name: "",
|
|
355
|
+
source: "url"
|
|
356
|
+
});
|
|
357
|
+
})));
|
|
244
358
|
}
|
|
245
359
|
|
|
246
360
|
//#endregion
|
|
@@ -255,7 +369,7 @@ function vueContextToAttributes(context) {
|
|
|
255
369
|
};
|
|
256
370
|
if (context.vue.componentProps) vue.componentProps = context.vue.componentProps;
|
|
257
371
|
if (context.vue.route) vue.route = context.vue.route;
|
|
258
|
-
return
|
|
372
|
+
return toCustomContext("vue", vue);
|
|
259
373
|
}
|
|
260
374
|
function vueWarningContextToAttributes(context) {
|
|
261
375
|
const vue = {
|
|
@@ -265,7 +379,7 @@ function vueWarningContextToAttributes(context) {
|
|
|
265
379
|
componentTrace: context.vue.componentTrace
|
|
266
380
|
};
|
|
267
381
|
if (context.vue.route) vue.route = context.vue.route;
|
|
268
|
-
return
|
|
382
|
+
return toCustomContext("vue", vue);
|
|
269
383
|
}
|
|
270
384
|
const installedApps = /* @__PURE__ */ new WeakSet();
|
|
271
385
|
const flareVue = (app, options) => {
|
|
@@ -339,6 +453,15 @@ const flareVue = (app, options) => {
|
|
|
339
453
|
if (typeof initialWarnHandler === "function") initialWarnHandler(msg, instance, trace);
|
|
340
454
|
};
|
|
341
455
|
}
|
|
456
|
+
if (options?.router) try {
|
|
457
|
+
const stopRouterTracing = traceVueRouter(options.router);
|
|
458
|
+
if (typeof app.onUnmount === "function") app.onUnmount(stopRouterTracing);
|
|
459
|
+
} catch {}
|
|
460
|
+
const profile = options?.profileComponents;
|
|
461
|
+
if (!(profile === true || Array.isArray(profile) && profile.length > 0)) return;
|
|
462
|
+
try {
|
|
463
|
+
app.mixin(createComponentProfilerMixin(createComponentMatcher(profile)));
|
|
464
|
+
} catch {}
|
|
342
465
|
};
|
|
343
466
|
|
|
344
467
|
//#endregion
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as vue from "vue";
|
|
2
2
|
import { ComponentPublicInstance, Plugin, PropType } from "vue";
|
|
3
3
|
import { Flare } from "@flareapp/js/browser";
|
|
4
|
+
import { ProfileComponentsOption } from "@flareapp/core/util";
|
|
4
5
|
|
|
5
6
|
//#region src/types.d.ts
|
|
6
7
|
type ErrorOrigin = 'setup' | 'render' | 'lifecycle' | 'event' | 'watcher' | 'unknown';
|
|
@@ -46,7 +47,14 @@ type FlareErrorBoundaryHookParams = {
|
|
|
46
47
|
info: string;
|
|
47
48
|
};
|
|
48
49
|
type FlareVueOptions = {
|
|
49
|
-
flare?: Flare;
|
|
50
|
+
flare?: Flare; /** A vue-router Router instance. When set, enables navigation/pageload performance tracing. */
|
|
51
|
+
router?: unknown;
|
|
52
|
+
/**
|
|
53
|
+
* Record a span per component mount. An array matches component names exactly (string) or by
|
|
54
|
+
* `test()` (RegExp). `true` profiles every named component, which is a debugging aid: a real page
|
|
55
|
+
* will hit `maxSpansPerTrace` and bury the useful spans. Requires `enableTracing`.
|
|
56
|
+
*/
|
|
57
|
+
profileComponents?: ProfileComponentsOption;
|
|
50
58
|
captureWarnings?: boolean;
|
|
51
59
|
attachProps?: boolean;
|
|
52
60
|
propsMaxDepth?: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { Flare } from "@flareapp/js/browser";
|
|
1
2
|
import * as vue from "vue";
|
|
2
3
|
import { ComponentPublicInstance, Plugin, PropType } from "vue";
|
|
3
|
-
import {
|
|
4
|
+
import { ProfileComponentsOption } from "@flareapp/core/util";
|
|
4
5
|
|
|
5
6
|
//#region src/types.d.ts
|
|
6
7
|
type ErrorOrigin = 'setup' | 'render' | 'lifecycle' | 'event' | 'watcher' | 'unknown';
|
|
@@ -46,7 +47,14 @@ type FlareErrorBoundaryHookParams = {
|
|
|
46
47
|
info: string;
|
|
47
48
|
};
|
|
48
49
|
type FlareVueOptions = {
|
|
49
|
-
flare?: Flare;
|
|
50
|
+
flare?: Flare; /** A vue-router Router instance. When set, enables navigation/pageload performance tracing. */
|
|
51
|
+
router?: unknown;
|
|
52
|
+
/**
|
|
53
|
+
* Record a span per component mount. An array matches component names exactly (string) or by
|
|
54
|
+
* `test()` (RegExp). `true` profiles every named component, which is a debugging aid: a real page
|
|
55
|
+
* will hit `maxSpansPerTrace` and bury the useful spans. Requires `enableTracing`.
|
|
56
|
+
*/
|
|
57
|
+
profileComponents?: ProfileComponentsOption;
|
|
50
58
|
captureWarnings?: boolean;
|
|
51
59
|
attachProps?: boolean;
|
|
52
60
|
propsMaxDepth?: number;
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_FlareErrorBoundary = require('./FlareErrorBoundary-
|
|
2
|
+
const require_FlareErrorBoundary = require('./FlareErrorBoundary-BndkpJFI.cjs');
|
|
3
3
|
let _flareapp_js = require("@flareapp/js");
|
|
4
4
|
|
|
5
5
|
//#region src/index.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-
|
|
1
|
+
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-BFiYPDOl.cjs";
|
|
2
2
|
export { type ComponentHierarchyFrame, DEFAULT_PROPS_DENYLIST, type ErrorOrigin, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryHookParams, type FlareVueContext, type FlareVueOptions, type FlareVueWarningContext, type RouteContext, type RouteParamValue, type RouteQueryValue, flareVue };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-
|
|
1
|
+
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-CFWxnFRO.mjs";
|
|
2
2
|
export { type ComponentHierarchyFrame, DEFAULT_PROPS_DENYLIST, type ErrorOrigin, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryHookParams, type FlareVueContext, type FlareVueOptions, type FlareVueWarningContext, type RouteContext, type RouteParamValue, type RouteQueryValue, flareVue };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as registerDefaultFlare, n as flareVue, r as DEFAULT_PROPS_DENYLIST, t as FlareErrorBoundary } from "./FlareErrorBoundary-
|
|
1
|
+
import { i as registerDefaultFlare, n as flareVue, r as DEFAULT_PROPS_DENYLIST, t as FlareErrorBoundary } from "./FlareErrorBoundary-CO0ZV9lZ.mjs";
|
|
2
2
|
import { flare } from "@flareapp/js";
|
|
3
3
|
|
|
4
4
|
//#region src/index.ts
|
package/dist/inject.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_FlareErrorBoundary = require('./FlareErrorBoundary-
|
|
2
|
+
const require_FlareErrorBoundary = require('./FlareErrorBoundary-BndkpJFI.cjs');
|
|
3
3
|
|
|
4
4
|
exports.DEFAULT_PROPS_DENYLIST = require_FlareErrorBoundary.DEFAULT_PROPS_DENYLIST;
|
|
5
5
|
exports.FlareErrorBoundary = require_FlareErrorBoundary.FlareErrorBoundary;
|
package/dist/inject.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-
|
|
1
|
+
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-BFiYPDOl.cjs";
|
|
2
2
|
export { type ComponentHierarchyFrame, DEFAULT_PROPS_DENYLIST, type ErrorOrigin, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryHookParams, type FlareVueContext, type FlareVueOptions, type FlareVueWarningContext, type RouteContext, type RouteParamValue, type RouteQueryValue, flareVue };
|
package/dist/inject.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-
|
|
1
|
+
import { a as ErrorOrigin, c as FlareVueContext, d as RouteContext, f as RouteParamValue, i as ComponentHierarchyFrame, l as FlareVueOptions, n as flareVue, o as FlareErrorBoundaryFallbackProps, p as RouteQueryValue, r as FlareErrorBoundary, s as FlareErrorBoundaryHookParams, t as DEFAULT_PROPS_DENYLIST, u as FlareVueWarningContext } from "./constants-CFWxnFRO.mjs";
|
|
2
2
|
export { type ComponentHierarchyFrame, DEFAULT_PROPS_DENYLIST, type ErrorOrigin, FlareErrorBoundary, type FlareErrorBoundaryFallbackProps, type FlareErrorBoundaryHookParams, type FlareVueContext, type FlareVueOptions, type FlareVueWarningContext, type RouteContext, type RouteParamValue, type RouteQueryValue, flareVue };
|
package/dist/inject.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as flareVue, r as DEFAULT_PROPS_DENYLIST, t as FlareErrorBoundary } from "./FlareErrorBoundary-
|
|
1
|
+
import { n as flareVue, r as DEFAULT_PROPS_DENYLIST, t as FlareErrorBoundary } from "./FlareErrorBoundary-CO0ZV9lZ.mjs";
|
|
2
2
|
|
|
3
3
|
export { DEFAULT_PROPS_DENYLIST, FlareErrorBoundary, flareVue };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flareapp/vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Vue client for flareapp.io",
|
|
5
5
|
"homepage": "https://flareapp.io",
|
|
6
6
|
"bugs": "https://github.com/spatie/flare-client-js/issues",
|
|
@@ -62,11 +62,12 @@
|
|
|
62
62
|
"verify:inject": "node scripts/verify-inject-no-root.mjs"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@flareapp/core": "2.
|
|
65
|
+
"@flareapp/core": "2.8.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@flareapp/electron": "file:../electron",
|
|
69
69
|
"@flareapp/js": "file:../js",
|
|
70
|
+
"@flareapp/test-helpers": "*",
|
|
70
71
|
"@vue/test-utils": "^2.4.0",
|
|
71
72
|
"jsdom": "^26.1.0",
|
|
72
73
|
"tsdown": "^0.20.3",
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
"vue-router": "^5.0.0"
|
|
77
78
|
},
|
|
78
79
|
"peerDependencies": {
|
|
79
|
-
"@flareapp/js": "^2.
|
|
80
|
+
"@flareapp/js": "^2.8.0",
|
|
80
81
|
"vue": "^3.0.0",
|
|
81
82
|
"vue-router": "^4.0.0 || ^5.0.0"
|
|
82
83
|
},
|