Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm install @exceptionless/react-native @react-native-async-storage/async-storag
cd ios && pod install
```

For Expo development or standalone builds, add the config plugin:
No config plugin is required: the native crash reporter pod is linked by autolinking. The optional config plugin currently makes no native changes:

```json
{
Expand Down Expand Up @@ -104,7 +104,7 @@ await Exceptionless.createException(new Error("Checkout failed"))

## Troubleshooting

- If native crashes do not appear in Expo, verify the app is not running in Expo Go and that the config plugin is present before rebuilding the native app.
- If native crashes do not appear in Expo, verify the app is not running in Expo Go, rebuild the native app after installing the package, and check that `ios/Podfile.lock` includes `exceptionless-react-native` (autolinking).
- If simulator submissions cannot reach a local Exceptionless server, use `http://localhost:<port>` for iOS Simulator and make sure Metro is not running in Expo's default LAN mode. Physical devices need a reachable LAN host.
- For malformed or unexpected stacks, verify behavior in `ReactNativeErrorPlugin` tests before changing parser logic.
- For native crash report loss concerns, verify `NativeCrashPlugin` only clears pending reports after at least one report is retrieved and submitted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ app.config.errorHandler = ExceptionlessErrorHandler;
app.mount("#app");
```

`ExceptionlessErrorHandler` adds the component name (`component`) and Vue's error source (`info`) to each
error. It does not include the component instance.

## Send

```js
Expand Down
4 changes: 2 additions & 2 deletions packages/node/src/plugins/NodeRequestInfoPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class NodeRequestInfoPlugin implements IEventPlugin {
hostname: string;
path: string;
headers: Record<string, string>;
params: Record<string, unknown>;
query?: Record<string, unknown>;
body?: object;
};

Expand Down Expand Up @@ -81,7 +81,7 @@ export class NodeRequestInfoPlugin implements IEventPlugin {
}

if (config.includeQueryString) {
const json = stringify(request.params, exclusions);
const json = stringify(request.query, exclusions);
if (!isEmpty(json)) {
requestInfo.query_string = JSON.parse(json) as Record<string, string>;
}
Expand Down
65 changes: 65 additions & 0 deletions packages/node/test/plugins/NodeRequestInfoPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, test } from "vitest";

import { EventContext, EventPluginContext, ExceptionlessClient, KnownEventDataKeys, RequestInfo } from "@exceptionless/core";

import { NodeRequestInfoPlugin } from "../../src/plugins/NodeRequestInfoPlugin.js";

describe("NodeRequestInfoPlugin", () => {
const run = async (request: Record<string, unknown>): Promise<RequestInfo | undefined> => {
const client = new ExceptionlessClient();
const eventContext = new EventContext();
eventContext[KnownEventDataKeys.RequestInfo] = request;

const context = new EventPluginContext(
client,
{
data: {}
},
eventContext
);
await new NodeRequestInfoPlugin().run(context);
return context.event.data?.[KnownEventDataKeys.RequestInfo];
};

test("should set query_string from the request query instead of route params", async () => {
const requestInfo = await run({
method: "GET",
secure: false,
ip: "127.0.0.1",
hostname: "localhost",
path: "/users/42",
headers: {
host: "localhost:3000",
"user-agent": "test"
},
params: {
id: "42"
},
query: {
search: "abc",
page: "2"
}
});

expect(requestInfo?.path).toBe("/users/42");
expect(requestInfo?.query_string).toEqual({
search: "abc",
page: "2"
});
});

test("should omit query_string when the request has no query", async () => {
const requestInfo = await run({
method: "GET",
path: "/users/42",
headers: {
"user-agent": "test"
},
params: {
id: "42"
}
});

expect(requestInfo?.query_string).toBeUndefined();
});
});
2 changes: 1 addition & 1 deletion packages/react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ That's it. Unhandled errors, promise rejections, and native iOS crashes are now
npx expo install @exceptionless/react-native @react-native-async-storage/async-storage
```

Add the config plugin to `app.json` when using development or standalone builds:
The native crash reporter is linked by autolinking, so no config plugin is required. The package ships an optional config plugin that currently makes no native changes; you can add it to `app.json` so future native configuration is applied automatically:

```json
{
Expand Down
13 changes: 2 additions & 11 deletions packages/react-native/src/ReactNativeExceptionlessClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export class ReactNativeExceptionlessClient extends ExceptionlessClient {
const config = this.config;

if (configurationOrApiKey && !this._initialized) {
this.configureStorage(config);
config.services.storage = new AsyncStorageProvider();
config.usePersistedQueueStorage = true;

if (Platform.OS === "ios") {
config.addPlugin(new NativeCrashPlugin());
Expand All @@ -28,14 +29,4 @@ export class ReactNativeExceptionlessClient extends ExceptionlessClient {

await super.startup(configurationOrApiKey);
}

private configureStorage(config: Configuration): void {
try {
const storage = new AsyncStorageProvider();
config.services.storage = storage;
config.usePersistedQueueStorage = true;
} catch {
config.services.log.info("AsyncStorage provider failed to initialize. Using in-memory storage. Events will not persist across app restarts.");
}
}
}
3 changes: 3 additions & 0 deletions packages/vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ app.config.errorHandler = ExceptionlessErrorHandler;
app.mount("#app");
```

`ExceptionlessErrorHandler` adds the component name (`component`) and Vue's error source (`info`) to each
error. It does not include the component instance.

## Handling Events

While errors within the components themselves are automatically sent to
Expand Down
36 changes: 31 additions & 5 deletions packages/vue/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
export * from "@exceptionless/browser";
import { Exceptionless } from "@exceptionless/browser";
import { Exceptionless, toError } from "@exceptionless/browser";

interface VueComponentOptions {
name?: unknown;
__name?: unknown;
_componentTag?: unknown;
}

/**
* https://vuejs.org/v2/api/#errorHandler
* https://v3.vuejs.org/api/application-config.html#errorhandler
* Gets the component name without walking the component instance, which references the whole app (and the DOM).
*/
function getComponentName(vm: unknown): string | undefined {
if (typeof vm !== "object" || vm === null) {
return undefined;
}

const options = (vm as { $options?: VueComponentOptions }).$options;
const name = options?.name ?? options?.__name ?? options?._componentTag;
return typeof name === "string" && name ? name : undefined;
}

/**
* https://vuejs.org/api/application.html#app-config-errorhandler
* https://v2.vuejs.org/v2/api/#errorHandler
* @param err
* @param vm
* @param info
*/
export const ExceptionlessErrorHandler = async (err: Error, vm: unknown, info: unknown): Promise<void> => {
await Exceptionless.createException(err).setProperty("vm", vm).setProperty("info", info).submit();
export const ExceptionlessErrorHandler = async (err: unknown, vm: unknown, info: unknown): Promise<void> => {
const builder = Exceptionless.createException(toError(err));

const component = getComponentName(vm);
if (component) {
builder.setProperty("component", component);
}

await builder.setProperty("info", info).submit();
};
82 changes: 82 additions & 0 deletions packages/vue/test/ExceptionlessErrorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, test, vi } from "vitest";

import { ExceptionlessErrorHandler } from "../src/index.js";

const mocks = vi.hoisted(() => ({
createException: vi.fn(),
setProperty: vi.fn(),
submit: vi.fn().mockResolvedValue(undefined)
}));

vi.mock("@exceptionless/browser", () => {
const builder = {
setProperty: mocks.setProperty.mockImplementation(() => builder),
submit: mocks.submit
};

return {
Exceptionless: {
createException: mocks.createException.mockImplementation(() => builder)
},
toError: (value: unknown) => (value instanceof Error ? value : new Error(String(value)))
};
});

describe("ExceptionlessErrorHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
});

test("should record the component name and info instead of the component instance", async () => {
const root: Record<string, unknown> = {
$options: {
name: "App"
}
};
const vm = {
$options: {
__name: "Broken"
},
$root: root,
$parent: root
};
root["$children"] = [vm];
const error = new Error("Boom");

await ExceptionlessErrorHandler(error, vm, "render function");

expect(mocks.createException).toHaveBeenCalledWith(error);
expect(mocks.setProperty.mock.calls).toEqual([
["component", "Broken"],
["info", "render function"]
]);
expect(mocks.submit).toHaveBeenCalled();
});

test("should prefer the explicit component name", async () => {
await ExceptionlessErrorHandler(
new Error("Boom"),
{
$options: {
name: "Named",
__name: "File"
}
},
"setup function"
);

expect(mocks.setProperty).toHaveBeenCalledWith("component", "Named");
});

test("should handle a missing instance and a non-error value", async () => {
await ExceptionlessErrorHandler("Boom", null, "watcher callback");

expect(mocks.createException).toHaveBeenCalledWith(
expect.objectContaining({
message: "Boom"
})
);
expect(mocks.setProperty.mock.calls).toEqual([["info", "watcher callback"]]);
expect(mocks.submit).toHaveBeenCalled();
});
});
19 changes: 19 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ export default defineConfig({
}
}
},
{
test: {
name: "vue",
root: "packages/vue",
environment: "jsdom",
environmentOptions: {
jsdom: {
url: "http://localhost/"
}
}
},
resolve: {
conditions: ["source"],
alias: {
"@exceptionless/core": path.resolve(__dirname, "packages/core/src"),
"@exceptionless/browser": path.resolve(__dirname, "packages/browser/src")
}
}
},
{
test: {
name: "react-native",
Expand Down
Loading