Skip to content

Vue ExceptionlessErrorHandler serializes the whole component instance and freezes the page #203

Open

Description

@ejsmith

ExceptionlessErrorHandler in @exceptionless/vue 3.2.1 adds the Vue component instance to the event with setProperty("vm", vm). Serializing a component instance walks $root, $parent, $el (DOM nodes), the app context and every reactive object reachable from them, so the first component error blocks the main thread for seconds and produces tens of megabytes of event data. In a real app the tab hangs and then crashes.

Where

What happens

Measured in Node 22 with jsdom, Vue 3.5 and @exceptionless/core 3.2.1, calling only the two setProperty calls the handler makes (before submit(), which serializes the event again and writes it to the persisted queue):

Components on the page setProperty("vm") time Serialized vm size
2 0.5 s 9.8 MB
7 1.1 s 23.7 MB
22 1.9 s 42.1 MB
52 3.5 s 80.7 MB (900 MB RSS)

Results are the same with NODE_ENV=production. Vue also logs "Property "toJSON" was accessed during render but is not defined on instance" while the instance proxy is walked.

The handler's signature (err: Error, vm: unknown, info: unknown) => Promise<void> also doesn't match Vue's errorHandler type, so the documented app.config.errorHandler = ExceptionlessErrorHandler fails to compile in a strict TypeScript main.ts:

error TS2322: Type '(err: Error, vm: unknown, info: unknown) => Promise<void>' is not assignable to type '(err: unknown, instance: ComponentPublicInstance<...> | null, info: string) => void'.
  Types of parameters 'err' and 'err' are incompatible.
    Type 'unknown' is not assignable to type 'Error'.

Expected

A component error is reported quickly, with a small amount of data that identifies the component, and the page stays responsive.

Steps to reproduce

  1. Create a Vue 3 app, call Exceptionless.startup(...) and set app.config.errorHandler = ExceptionlessErrorHandler as shown in the README.
  2. Render a component whose render function or setup throws.
  3. The tab stops responding on the first error, and on a page with more than a handful of components it runs out of memory.

Minimal Node repro of the serialization cost (uses vue, jsdom and @exceptionless/core@3.2.1):

import { JSDOM } from "jsdom";
const dom = new JSDOM("<!doctype html><div id=app></div>", { url: "http://localhost/" });
Object.assign(globalThis, { window: dom.window, document: dom.window.document, Element: dom.window.Element, SVGElement: dom.window.SVGElement });
const { createApp, h } = await import("vue");
const { ExceptionlessClient } = await import("@exceptionless/core");
const client = new ExceptionlessClient();

const Broken = { render() { throw new Error("boom"); } };
const app = createApp({ render: () => h("div", [h("span", "hi"), h(Broken)]) });
app.config.errorHandler = (err, vm, info) => {
  const start = Date.now();
  const builder = client.createException(err).setProperty("vm", vm).setProperty("info", info);
  console.log(`${Date.now() - start}ms, ${JSON.stringify(builder.target.data.vm).length} bytes`);
};
app.mount("#app");

Suggested fix

Don't serialize the instance. Record a few safe fields instead, such as the component name (vm?.$options?.name ?? vm?.$options?.__name) and info. Change the signature to (err: unknown, instance: ComponentPublicInstance | null, info: string) => void (submitting without returning the promise) so it can be assigned to app.config.errorHandler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions