Custom Events for Advanced React Monitoring
Standard error monitoring (exceptions, promise rejections) covers most cases, but production apps often need to track custom events: a checkout that fails silently, a feature adoption rate, a performance metric specific to your app. Sentry's custom event APIs let you emit arbitrary events with attached context, allowing you to build rich observability into your React app beyond exceptions.
Emitting Custom Errors and Messages
Beyond automatic exception capture, use Sentry.captureException() and Sentry.captureMessage() to log custom events:
import * as Sentry from "@sentry/react";
function handlePaymentFailure(error, retryCount) {
// Capture as an error event
Sentry.captureException(error, {
tags: {
feature: "checkout",
payment_provider: "stripe",
},
contexts: {
payment: {
retryCount,
amount: 149.99,
currency: "USD",
},
},
level: "error",
});
}
function logUserAction(action, details) {
// Capture as a message event
Sentry.captureMessage(`User action: ${action}`, "info");
// Attach context
Sentry.addBreadcrumb({
category: "user_action",
message: action,
data: details,
level: "info",
});
}
// Usage
handlePaymentFailure(paymentError, 2);
logUserAction("feature_adopted", { featureName: "darkMode" });
These events appear in Sentry alongside automatic errors, with full context and breadcrumbs.
Tracking Custom Metrics and Performance
Use Sentry.startTransaction() to measure custom performance metrics:
import * as Sentry from "@sentry/react";
function CheckoutForm() {
const handleSubmit = async (e) => {
e.preventDefault();
// Start a transaction for the entire checkout flow
const transaction = Sentry.startTransaction({
op: "checkout",
name: "Checkout Process",
description: "Complete checkout from form to confirmation",
});
try {
// Span 1: Validate form
const validateSpan = transaction.startChild({
op: "validate",
description: "Validate checkout form",
});
validateCheckoutForm(formData);
validateSpan.finish();
// Span 2: Call payment API
const paymentSpan = transaction.startChild({
op: "http.client",
description: "POST /api/payments",
});
const paymentResult = await submitPayment(formData);
paymentSpan.setData("status", paymentResult.status);
paymentSpan.finish();
// Span 3: Create order in database
const orderSpan = transaction.startChild({
op: "db.query",
description: "INSERT INTO orders",
});
const order = await createOrder(paymentResult);
orderSpan.setData("orderId", order.id);
orderSpan.finish();
// Transaction complete
transaction.finish();
Sentry.captureMessage("Checkout successful", "info");
} catch (error) {
transaction.setStatus("error");
transaction.finish();
Sentry.captureException(error, {
tags: { feature: "checkout" },
level: "error",
});
}
};
return form onSubmit={handleSubmit}>
{/* form fields */}
/form>;
}
This produces a detailed performance waterfall in Sentry:
Checkout Process (1200ms total)
├─ Validate form (50ms)
├─ POST /api/payments (400ms) — status: 200
├─ INSERT INTO orders (300ms) — orderId: order-123
└─ Navigate to confirmation (50ms)
You can see which step is slow and optimize accordingly.
Filtering and Sampling Custom Events
Custom events can be noisy. Use beforeSend to filter or sample them:
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
beforeSend(event, hint) {
// Do not send debug-level messages in production
if (event.level === "debug" && process.env.NODE_ENV === "production") {
return null; // Drop the event
}
// Sample business events: send 10% to reduce noise
if (event.tags?.category === "business_event") {
if (Math.random() > 0.1) {
return null;
}
}
// Redact sensitive data
if (event.request?.url?.includes("password")) {
event.request.url = "[REDACTED]";
}
return event;
},
});
This prevents log spam while maintaining visibility into critical events.
Example: Tracking Feature Adoption
Use custom events to track feature adoption:
import * as Sentry from "@sentry/react";
function FeatureGate({ feature, children }) {
const isEnabled = checkFeatureFlag(feature);
useEffect(() => {
if (isEnabled) {
Sentry.captureMessage(
`Feature enabled: ${feature}`,
"info"
);
Sentry.addBreadcrumb({
category: "feature_flag",
message: feature,
data: {
enabled: true,
userId: getCurrentUserId(),
timestamp: new Date().toISOString(),
},
});
}
}, [feature, isEnabled]);
return isEnabled ? children : null;
}
// Usage
function App() {
return (
div>
<FeatureGate feature="darkMode">
<DarkModeToggle />
</FeatureGate>
<FeatureGate feature="advancedSearch">
<AdvancedSearchForm />
</FeatureGate>
/div>
);
}
In Sentry, you can then query: "How many users saw the darkMode feature?" by searching for the breadcrumb event.
Tracking Business Metrics
Track revenue, conversions, and user engagement:
function trackCheckoutStep(step, amount) {
Sentry.captureMessage(`Checkout step: ${step}`, "info");
Sentry.addBreadcrumb({
category: "checkout",
message: step,
data: {
step,
amount,
cartItems: getCartItemCount(),
conversionStage: "checkout",
},
});
}
function handlePurchase(orderId, amount) {
const transaction = Sentry.startTransaction({
op: "purchase",
name: "Purchase Completion",
});
try {
// Process payment
const result = await processPayment(orderId, amount);
transaction.setData("amount", amount);
transaction.setData("orderId", orderId);
transaction.setStatus("ok");
Sentry.captureMessage("Purchase completed", "info");
return result;
} catch (error) {
transaction.setStatus("error");
Sentry.captureException(error);
throw error;
} finally {
transaction.finish();
}
}
// Track in your analytics dashboard
trackCheckoutStep("cart_viewed", 0);
trackCheckoutStep("shipping_entered", 149.99);
trackCheckoutStep("payment_entered", 149.99);
const result = await handlePurchase("order-123", 149.99);
trackCheckoutStep("order_confirmed", 149.99);
These events create a detailed audit trail of user behavior and revenue impact.
Using Custom Context for Better Debugging
Attach custom context to all events to aid debugging:
Sentry.setContext("user_session", {
sessionId: generateUUID(),
startTime: Date.now(),
userAgent: navigator.userAgent,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: navigator.language,
colorScheme: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
});
Sentry.setContext("app_state", {
route: currentRoute,
isOnline: navigator.onLine,
hasActiveSession: user !== null,
cachedDataSize: getCacheSize(),
});
Sentry.setContext("performance_metrics", {
pageLoadTime: getPageLoadTime(),
timeToInteractive: getTimeToInteractive(),
firstContentfulPaint: getFirstContentfulPaint(),
});
Now every event includes this context, making debugging much easier.
Advanced Filtering with Dynamic Sampling
For high-traffic apps, dynamically adjust sampling based on error rate:
const getSampleRate = () => {
const errorRate = getRecentErrorRate(); // 0–1
// If error rate is high, sample less to see details
if (errorRate > 0.1) return 0.5; // 50% sampling
if (errorRate > 0.05) return 0.3; // 30% sampling
return 0.1; // 10% sampling
};
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
tracesSampleRate: getSampleRate(),
});
// Recalculate every hour
setInterval(() => {
Sentry.init({
tracesSampleRate: getSampleRate(),
});
}, 3600000);
This balances detail (high sampling during outages) with cost (low sampling during normal operation).
Querying Custom Events in Sentry
After emitting custom events, query them in Sentry:
# Find all checkout-related events
event.tags.feature: checkout
# Find errors in a specific feature
level: error AND event.contexts.feature: darkMode
# Find performance issues
event.type: transaction AND event.transaction: "Checkout Process"
# Find business events
event.tags.category: business_event AND event.level: info
Use these queries to build dashboards:
- Checkout conversion funnel (cart_viewed → shipping_entered → payment_entered → order_confirmed).
- Feature adoption rate (count events where feature is enabled).
- Revenue per purchase (sum of event.contexts.amount).
- Performance regression (transaction duration trend over time).
Key Takeaways
- Use
Sentry.captureException()andSentry.captureMessage()to emit custom events beyond automatic error capture. - Track performance with
Sentry.startTransaction()to measure custom metrics and identify bottlenecks. - Attach context (user, session, feature flags) to all events for better debugging.
- Filter and sample custom events to balance visibility with cost.
- Query custom events to build business dashboards and track adoption, conversion, and revenue.
Frequently Asked Questions
Does custom event emission increase costs?
Yes, custom events count toward your Sentry quota. Sample them using beforeSend to keep costs manageable. A typical app emits 10–50 custom events per user per day; most teams filter this to 1–5 per user per day.
Can I emit custom events without triggering alerts?
Yes. Custom messages (info/debug level) do not trigger alerts by default. Only errors and exceptions do. If you want to alert on custom events, create a specific alert rule for them.
What is the difference between captureMessage and captureException?
captureException is for errors (has a stack trace). captureMessage is for informational messages (no stack trace). Use captureException for bugs, captureMessage for business events.
Can I correlate custom events with errors?
Yes. Include a correlation ID in both custom events and errors:
const correlationId = generateUUID();
Sentry.captureMessage("Payment initiated", "info");
Sentry.setContext("payment", { correlationId });
// Later
handlePaymentError(error, correlationId);
Then query for all events with the same correlation ID.
How do I prevent custom events from being sent in development?
Check the environment:
Sentry.captureMessage(
"Checkout step",
"info"
{
environment: process.env.NODE_ENV,
}
);
// In Sentry config
Sentry.init({
environment: process.env.NODE_ENV,
ignoreErrors: (error) => process.env.NODE_ENV === "development",
});
Or disable Sentry entirely in development:
if (process.env.NODE_ENV === "production") {
Sentry.init({ ... });
}