npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@upreport/oops-widget

v1.0.0-next.6

Published

Clearly communicate system status and errors to users, improving experience and reducing support.

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:

  1. Be implemented as a Web Component (Custom Element)
  2. 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 providing getStatusDetails(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.