Skip to content

Commit 68302ce

Browse files
committed
refactor: Enhance examples and add documentation tests
Consolidates the React examples into a single `App.jsx` to improve clarity and maintenance, deprecating `HooksExampleApp.js`. Adds SEO metadata to Next.js App Router pages. Introduces dedicated tests for documented core and browser client functionalities, ensuring consistency and validating usage patterns. Makes `EventBuilder.markAsCritical` parameter optional, defaulting to `true` for improved usability. Includes minor corrections in the root `README.md` and internalizes a `next-request.js` helper function.
1 parent e366114 commit 68302ce

12 files changed

Lines changed: 211 additions & 107 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,9 @@ import { Exceptionless } from "@exceptionless/browser";
150150
await Exceptionless.submitLog("Logging made easy");
151151

152152
// You can also specify the log source and log level.
153-
// We recommend specifying one of the following log levels: Trace, Debug, Info, Warn, Error
154-
await Exceptionless.submitLog("app.logger", "This is so easy", "Info");
155-
await Exceptionless.createLog("app.logger", "This is so easy", "Info").addTags("Exceptionless").submit();
153+
// We recommend specifying one of the following log levels: trace, debug, info, warn, error
154+
await Exceptionless.submitLog("app.logger", "This is so easy", "info");
155+
await Exceptionless.createLog("app.logger", "This is so easy", "info").addTags("Exceptionless").submit();
156156

157157
// Submit feature usages
158158
await Exceptionless.submitFeatureUsage("MyFeature");
@@ -163,7 +163,7 @@ await Exceptionless.submitNotFound("/somepage");
163163
await Exceptionless.createNotFound("/somepage").addTags("Exceptionless").submit();
164164

165165
// Submit a custom event type
166-
await Exceptionless.submitEvent({ message = "Low Fuel", type = "racecar", source = "Fuel System" });
166+
await Exceptionless.submitEvent({ message: "Low Fuel", type: "racecar", source: "Fuel System" });
167167
```
168168

169169
#### Manually submitting Errors

example/nextjs/app/page.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import ClientDemoPanel from "../components/ClientDemoPanel.jsx";
22

3+
export const metadata = {
4+
title: "Exceptionless Next.js Example",
5+
description: "Client and server Exceptionless integration demo for the Next.js App Router."
6+
};
7+
38
export default function HomePage() {
49
const deploymentTarget = process.env.VERCEL_ENV ?? "local";
510

example/nextjs/app/server-component-error/page.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
export const dynamic = "force-dynamic";
2+
export const metadata = {
3+
title: "Server Component Error Demo",
4+
description: "Demonstration route for Exceptionless App Router server component failures."
5+
};
26

37
export default function ServerComponentErrorPage() {
48
throw new Error("Server component crash from the Exceptionless Next.js demo");

example/nextjs/lib/next-request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function buildRequestContextFromOnRequestError(request) {
1515
});
1616
}
1717

18-
export function buildRequestContext({ method, pathOrUrl, headers, body }) {
18+
function buildRequestContext({ method, pathOrUrl, headers, body }) {
1919
const normalizedHeaders = normalizeHeaders(headers);
2020
const origin = getOrigin(normalizedHeaders);
2121
const url = new URL(pathOrUrl, origin);

example/react/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## Exceptionless React Example
22

3-
This example shows how to use the `@exceptionless/react` package. There is both a class component example (App.js) and a function component example with hooks (HooksExampleApp.js).
3+
This example shows how to use the `@exceptionless/react` package with an error boundary around a component that can throw during render.
44

55
The package includes [error boundary support](https://reactjs.org/docs/error-boundaries.html) which means uncaught errors inside your components will automatically be sent to Exceptionless.
66

example/react/src/App.jsx

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@ import React, { Component } from "react";
22
import "./App.css";
33
import { Exceptionless, ExceptionlessErrorBoundary } from "@exceptionless/react";
44

5+
function ExceptionlessExampleContent({ error, message, errorInfo, onThrowComponentError, onUnhandledException, onSubmitMessage, onTryCatchExample }) {
6+
if (error) {
7+
throw new Error("I crashed!");
8+
}
9+
10+
return (
11+
<div className="App">
12+
<header className="App-header">
13+
<div className="container">
14+
<h1 className="App-title">Exceptionless React Sample</h1>
15+
<p>By pressing the button below, an uncaught error will be thrown inside your component. This will automatically be sent to Exceptionless.</p>
16+
<button onClick={onThrowComponentError}>Simulate Error</button>
17+
<div>
18+
<p>Throw an uncaught error and make sure Exceptionless tracks it.</p>
19+
<button onClick={onUnhandledException}>Throw unhandled error</button>
20+
</div>
21+
<p>The following buttons simulated handled events outside the component.</p>
22+
<button onClick={onSubmitMessage}>Submit Message</button>
23+
{message && (
24+
<p>
25+
Message sent to Exceptionless: <code>{message}</code>
26+
</p>
27+
)}
28+
<button onClick={onTryCatchExample}>Try/Catch Example</button>
29+
{errorInfo && (
30+
<p>
31+
Error message sent to Exceptionless: <code>{errorInfo}</code>
32+
</p>
33+
)}
34+
</div>
35+
</header>
36+
</div>
37+
);
38+
}
39+
540
class App extends Component {
641
constructor(props) {
742
super(props);
@@ -46,43 +81,24 @@ class App extends Component {
4681
throw new Error("Unhandled exception");
4782
};
4883

49-
renderExample = () => {
50-
if (this.state.error) {
51-
throw new Error("I crashed!");
52-
} else {
53-
return (
54-
<div className="App">
55-
<header className="App-header">
56-
<div className="container">
57-
<h1 className="App-title">Exceptionless React Sample</h1>
58-
<p>By pressing the button below, an uncaught error will be thrown inside your component. This will automatically be sent to Exceptionless.</p>
59-
<button onClick={this.throwErrorInComponent}>Simulate Error</button>
60-
<div>
61-
<p>Throw an uncaught error and make sure Exceptionless tracks it.</p>
62-
<button onClick={this.unhandledExceptionExample}>Throw unhandled error</button>
63-
</div>
64-
<p>The following buttons simulated handled events outside the component.</p>
65-
<button onClick={this.submitMessage}>Submit Message</button>
66-
{this.state.message && (
67-
<p>
68-
Message sent to Exceptionless: <code>{this.state.message}</code>
69-
</p>
70-
)}
71-
<button onClick={this.tryCatchExample}>Try/Catch Example</button>
72-
{this.state.errorInfo && (
73-
<p>
74-
Error message sent to Exceptionless: <code>{this.state.errorInfo}</code>
75-
</p>
76-
)}
77-
</div>
78-
</header>
79-
</div>
80-
);
81-
}
82-
};
83-
8484
render() {
85-
return <ExceptionlessErrorBoundary>{this.renderExample()}</ExceptionlessErrorBoundary>;
85+
return (
86+
<ExceptionlessErrorBoundary>
87+
<ExceptionlessExampleContent
88+
error={this.state.error}
89+
message={this.state.message}
90+
errorInfo={this.state.errorInfo}
91+
onThrowComponentError={this.throwErrorInComponent}
92+
onUnhandledException={this.unhandledExceptionExample}
93+
onSubmitMessage={() => {
94+
void this.submitMessage();
95+
}}
96+
onTryCatchExample={() => {
97+
void this.tryCatchExample();
98+
}}
99+
/>
100+
</ExceptionlessErrorBoundary>
101+
);
86102
}
87103
}
88104

example/react/src/HooksExampleApp.js

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
import { BrowserExceptionlessClient } from "../src/BrowserExceptionlessClient.js";
4+
5+
describe("BrowserExceptionlessClient", () => {
6+
test("should configure documented browser plugins on first startup", async () => {
7+
const client = new BrowserExceptionlessClient();
8+
9+
await client.startup((config) => {
10+
config.apiKey = "UNIT_TEST_API_KEY";
11+
config.updateSettingsWhenIdleInterval = -1;
12+
});
13+
14+
try {
15+
const pluginNames = client.config.plugins.map((plugin) => plugin.name);
16+
17+
expect(pluginNames).toContain("BrowserGlobalHandlerPlugin");
18+
expect(pluginNames).toContain("BrowserIgnoreExtensionErrorsPlugin");
19+
expect(pluginNames).toContain("BrowserLifeCyclePlugin");
20+
expect(pluginNames).toContain("BrowserModuleInfoPlugin");
21+
expect(pluginNames).toContain("BrowserRequestInfoPlugin");
22+
expect(pluginNames).toContain("BrowserErrorPlugin");
23+
expect(pluginNames).not.toContain("SimpleErrorPlugin");
24+
} finally {
25+
await client.suspend();
26+
}
27+
});
28+
});

packages/core/src/EventBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export class EventBuilder {
194194
return this;
195195
}
196196

197-
public markAsCritical(critical: boolean): EventBuilder {
197+
public markAsCritical(critical: boolean = true): EventBuilder {
198198
if (critical) {
199199
this.addTags("Critical");
200200
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { describe, expect, test } from "vitest";
2+
3+
import { Configuration } from "../src/configuration/Configuration.js";
4+
import { ExceptionlessClient } from "../src/ExceptionlessClient.js";
5+
import { KnownEventDataKeys } from "../src/models/Event.js";
6+
7+
describe("documentation examples", () => {
8+
test("should configure production privacy and self-hosted endpoints", () => {
9+
const config = new Configuration();
10+
11+
config.apiKey = "UNIT_TEST_API_KEY";
12+
config.serverUrl = "https://collector.example.com";
13+
config.configServerUrl = "https://config.example.com";
14+
config.heartbeatServerUrl = "https://heartbeat.example.com";
15+
config.version = "1.2.3";
16+
config.defaultTags.push("core", "production");
17+
config.addDataExclusions("authorization", "cookie", "password", "secret", "set-cookie", "token");
18+
config.includePrivateInformation = false;
19+
20+
expect(config.isValid).toBe(true);
21+
expect(config.serverUrl).toBe("https://collector.example.com");
22+
expect(config.configServerUrl).toBe("https://config.example.com");
23+
expect(config.heartbeatServerUrl).toBe("https://heartbeat.example.com");
24+
expect(config.version).toBe("1.2.3");
25+
expect(config.defaultTags).toEqual(["core", "production"]);
26+
expect(config.dataExclusions).toEqual(["authorization", "cookie", "password", "secret", "set-cookie", "token"]);
27+
expect(config.includeCookies).toBe(false);
28+
expect(config.includeHeaders).toBe(false);
29+
expect(config.includeIpAddress).toBe(false);
30+
expect(config.includePostData).toBe(false);
31+
expect(config.includeQueryString).toBe(false);
32+
});
33+
34+
test("should build documented enriched exception events", () => {
35+
const client = new ExceptionlessClient();
36+
client.config.apiKey = "UNIT_TEST_API_KEY";
37+
client.config.addDataExclusions("creditCardNumber");
38+
39+
const error = new Error("Unable to create order from quote");
40+
const builder = client
41+
.createException(error)
42+
.setReferenceId("order-12345678")
43+
.setProperty(
44+
"Order",
45+
{
46+
id: "order-123",
47+
quoteId: 123,
48+
creditCardNumber: "4111111111111111"
49+
},
50+
4,
51+
["securityCode"]
52+
)
53+
.setProperty("Quote", 123)
54+
.addTags("orders")
55+
.markAsCritical()
56+
.setGeo(43.595089, -88.444602)
57+
.setUserIdentity("user-123", "Jane Doe")
58+
.setUserDescription("jane@example.com", "The submit button returned a blank page.");
59+
60+
expect(builder.target.type).toBe("error");
61+
expect(builder.target.reference_id).toBe("order-12345678");
62+
expect(builder.target.tags).toEqual(["orders", "Critical"]);
63+
expect(builder.target.geo).toBe("43.595089,-88.444602");
64+
expect(builder.target.data?.Order).toEqual({ id: "order-123", quoteId: 123 });
65+
expect(builder.target.data?.Quote).toBe(123);
66+
expect(builder.target.data?.[KnownEventDataKeys.UserInfo]).toEqual({ identity: "user-123", name: "Jane Doe" });
67+
expect(builder.target.data?.[KnownEventDataKeys.UserDescription]).toEqual({
68+
email_address: "jane@example.com",
69+
description: "The submit button returned a blank page."
70+
});
71+
});
72+
73+
test("should cancel events from documented runtime configuration plugins", async () => {
74+
const client = new ExceptionlessClient();
75+
client.config.apiKey = "UNIT_TEST_API_KEY";
76+
client.config.settings["enableCheckoutEvents"] = "false";
77+
78+
client.config.addPlugin("CheckoutEventToggle", 100, (context) => {
79+
const enabled = context.client.config.settings["enableCheckoutEvents"];
80+
81+
if (context.event.source === "checkout" && enabled === "false") {
82+
context.cancelled = true;
83+
}
84+
85+
return Promise.resolve();
86+
});
87+
88+
const context = await client.createLog("checkout", "Checkout opened", "info").submit();
89+
90+
expect(context.cancelled).toBe(true);
91+
});
92+
93+
test("should support documented session startup configuration", async () => {
94+
const client = new ExceptionlessClient();
95+
96+
await client.startup((config) => {
97+
config.apiKey = "UNIT_TEST_API_KEY";
98+
config.setUserIdentity("user-123", "Jane Doe");
99+
config.useSessions(true, 60000, true);
100+
config.updateSettingsWhenIdleInterval = -1;
101+
});
102+
103+
try {
104+
const context = await client.submitSessionStart();
105+
106+
expect(client.config.sessionsEnabled).toBe(true);
107+
expect(client.config.currentSessionIdentifier).toMatch(/^[0-9a-f]{32}$/);
108+
expect(context.event.reference_id).toBe(client.config.currentSessionIdentifier);
109+
} finally {
110+
await client.suspend();
111+
}
112+
});
113+
});

0 commit comments

Comments
 (0)