@upreport/oops-widget
v1.0.0-next.6
Published
Clearly communicate system status and errors to users, improving experience and reducing support.
Maintainers
Readme
@upreport/oops-widget
⚠️ EXPERIMENTAL: This is an experimental version and the API may change significantly in future releases.
Clearly communicate system status and errors to users, improving experience and reducing support.
📦 Installation
# Using npm
npm install @upreport/oops-widget
# Using pnpm
pnpm add @upreport/oops-widget
# Using yarn
yarn add @upreport/oops-widget✨ Features
- 🔄 Real-time system status monitoring
- 🎨 Customizable Web Component alert UI
- 🕸️ Fetch and XHR request interceptors for error and slow-request detection
- 🌐 Internationalization support with auto-detection
- 📱 Mobile-friendly placement and responsive behavior
- 📋 Optional incident details display with names, statuses, and links
⚡ Quickstart
import {
createOopsWidget,
LogLevel,
FetchInterceptor,
XHRInterceptor,
} from '@upreport/oops-widget';
import { StatusAlert } from '@upreport/oops-widget/statusAlert';
import { createAutoDetectI18nProvider } from '@upreport/oops-widget/i18n';
// Configure request interceptors
const interceptOptions = {
timeoutMs: 20000,
showSlowRequestAlerts: true,
showErrorAlerts: true,
monitoredServicePatterns: ['/api/*'],
monitoredServiceOptions: {
requireExplicitPatterns: true,
includeCurrentOrigin: true,
treatRelativeAsInternal: true,
},
};
const widget = createOopsWidget({
// Status polling endpoint (expects query param `s`)
statusUrl: 'https://status-api.example.com',
// Alert component to render
alertComponent: StatusAlert,
// Placement on screen: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
placement: 'bottom-right',
// Internationalization provider
i18nProvider: createAutoDetectI18nProvider(),
// Request interceptors
interceptors: [new FetchInterceptor(interceptOptions), new XHRInterceptor(interceptOptions)],
// Mobile-specific configuration
mobile: {
enabled: true,
placement: 'top',
},
// Logging verbosity
logLevel: LogLevel.DEBUG,
// Whether to display incident details when available
displayIncidentDetails: false,
});
widget
.start()
.then(() => console.log('Widget started'))
.catch((error) => console.error('Error starting widget:', error));⚙️ Configuration Options
OopsWidgetConfig
| Option | Type | Default | Description |
| ------------------------ | --------------------------------------------------------------------------------------- | -------------------- | ---------------------------------------------------------------------- |
| statusUrl | string | required | URL endpoint for polling system status (with query param s) |
| alertComponent | CustomElementConstructor | required | Web Component class for rendering alerts (e.g., StatusAlert) |
| placement | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'bottom-right' | Screen placement of the alert |
| i18nProvider | I18nProvider | required | Provider for localized status messages |
| interceptors | Interceptor[] | [] | Array of request interceptors (FetchInterceptor, XHRInterceptor) |
| mobile | { enabled: boolean; placement?: string; mediaQuery?: string } | { enabled: false } | Mobile-specific configuration |
| logLevel | LogLevel | LogLevel.INFO | Logging verbosity |
| alertClosureBehavior | Partial<Record<SystemStatus, { closeDurationMs?: number; checkIntervalMs?: number }>> | — | Custom timing for alert close and status check intervals |
| displayIncidentDetails | boolean | false | Whether to display incident details (names, statuses, links) in alerts |
InterceptorConfig
| Option | Type | Default | Description |
| -------------------------- | ---------- | ------- | ------------------------------------------------ |
| timeoutMs | number | 30000 | Milliseconds before a request is considered slow |
| showSlowRequestAlerts | boolean | true | Emit slow-request alerts |
| showErrorAlerts | boolean | true | Emit non-2xx HTTP status alerts |
| monitoredServicePatterns | string[] | [] | URL patterns to monitor |
| monitoredServiceOptions | object | {} | Options for monitored pattern matching |
monitoredServiceOptions
| Option | Type | Default | Description |
| ------------------------- | --------- | ------- | ------------------------------------------------------------------ |
| requireExplicitPatterns | boolean | false | Only monitor URLs explicitly matching patterns |
| includeCurrentOrigin | boolean | true | Include same-origin requests as monitored if no patterns specified |
| treatRelativeAsInternal | boolean | true | Treat relative URLs as internal (same-origin) |
📚 API Reference
createOopsWidget(config: OopsWidgetConfig): OopsWidgetInstance
Instantiate a new widget with the provided configuration.
Returns: OopsWidgetInstance with methods:
- 🚀
start(): Promise<void>— Perform initial status check and start monitoring. - 🧹
destroy(): void— Stop monitoring and cleanup resources.
FetchInterceptor & XHRInterceptor
Both implement the Interceptor interface:
interface Interceptor {
name: string;
setup(): () => void; // Returns cleanup function
}Use:
new FetchInterceptor(config);
new XHRInterceptor(config);Pass instances in the interceptors array to the widget.
🎨 UI Customization & Event System
The OopsWidget uses a web component-based UI system with a well-defined event architecture, allowing for complete customization of the alert appearance while maintaining functionality.
Built-in StatusAlert Component
The default StatusAlert component provides a ready-to-use alert UI with configurable themes for each status type. When using this component, everything is handled automatically.
Custom Alert Components
You can replace the built-in component with your own implementation. Custom components must:
- Be implemented as a Web Component (Custom Element)
- Handle the required attributes and events
Event Communication
The communication between the core widget and alert components happens through these custom events:
| Event Name | Direction | Description |
| -------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| oops-widget-closed | Alert → Core | Dispatched when user clicks the close button. The core widget handles recording the closure to prevent showing the same alert again too soon. |
| oops-widget-timer-start | Core → Alert | Sent when an alert timer starts. Includes a detail object with duration in milliseconds, used for progress animations. |
| oops-widget-timer-cancel | Core → Alert | Sent when a timer is cancelled, allowing the alert to reset its progress animation. |
Implementation Example
class MyCustomAlert extends HTMLElement {
private shadow: ShadowRoot;
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
}
// These attributes will be set by the core widget
static get observedAttributes(): string[] {
return [
'status-type', // Current system status (e.g. 'MajorOutage')
'title', // Alert title text
'message', // Alert message text
'placement', // Position on screen
'mobile-enabled', // Whether mobile mode is enabled
'mobile-placement', // Mobile-specific placement
'progress-duration', // For timer progress animation
'incident-names', // JSON string of incident names
'incident-statuses', // JSON string of incident statuses
'incident-links', // JSON string of incident detail links
'display-incident-details', // Whether to show incident details
'close-button-label', // Accessibility label for close button
];
}
connectedCallback(): void {
this.render();
this.setupEvents();
}
attributeChangedCallback(): void {
this.render(); // Re-render when any attribute changes
}
private setupEvents(): void {
// 1. Setup close button event
const closeBtn = this.shadow.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
// Dispatch event to notify core widget about closure
this.dispatchEvent(
new CustomEvent('oops-widget-closed', {
bubbles: true, // Must bubble up through the DOM
composed: true, // Must cross shadow DOM boundary
}),
);
});
}
// 2. Listen for timer events from core widget
this.addEventListener('oops-widget-timer-start', (e: Event) => {
const { duration } = (e as CustomEvent<{ duration: number }>).detail;
const progress = this.shadow.querySelector('.progress-bar');
if (progress instanceof HTMLElement) {
progress.style.transition = `width ${duration}ms linear`;
progress.style.width = '100%';
}
});
this.addEventListener('oops-widget-timer-cancel', () => {
const progress = this.shadow.querySelector('.progress-bar');
if (progress instanceof HTMLElement) {
progress.style.transition = 'none';
progress.style.width = '0';
}
});
}
private render(): void {
const status = this.getAttribute('status-type') || 'DegradedPerformance';
const title = this.getAttribute('title') || 'Status Alert';
const message = this.getAttribute('message') || '';
const closeLabel = this.getAttribute('close-button-label') || 'Close alert';
// Simple styling based on status
const colors: Record<string, string> = {
MajorOutage: '#f8d7da',
PartialOutage: '#ffebd6',
DegradedPerformance: '#fff3cd',
UnderMaintenance: '#e7f1fc',
Operational: '#d4edda',
};
const bgColor = colors[status] || colors.DegradedPerformance;
this.shadow.innerHTML = `
<style>
.alert {
background: ${bgColor};
padding: 16px;
border-radius: 8px;
position: relative;
overflow: hidden;
font-family: system-ui, sans-serif;
}
.header {
display: flex;
justify-content: space-between;
}
.close-btn {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
}
.progress-bar {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
width: 0;
background: rgba(0,0,0,0.2);
}
</style>
<div class="alert">
<div class="header">
<strong>${title}</strong>
<button class="close-btn" aria-label="${closeLabel}">×</button>
</div>
<p>${message}</p>
<div class="progress-bar"></div>
</div>
`;
}
}
// Use in widget configuration
const widget = createOopsWidget({
statusUrl: 'https://status-api.example.com',
alertComponent: MyCustomAlert,
// ... other options
});🔣 Types
SystemStatus: Enum of possible statuses (✅Operational, 🟡DegradedPerformance, 🟠PartialOutage, 🔴MajorOutage, 🔧UnderMaintenance).SystemStatusResponse:{ status: SystemStatus; incident?: IncidentInfo; rawData: Record<string, string> }.I18nProvider: Interface providinggetStatusDetails(status: SystemStatus): Promise<StatusDetails>.LogLevel:DEBUG | INFO | WARN | ERROR | NONE.
🌐 Internationalization
- 🔍
createAutoDetectI18nProvider(): Auto-detect browser locale. - 🌍
createStaticI18nProvider(translations: Record<string, StatusDetails>): I18nProvider: Provide custom translations.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
