LogRocket 前端监控

Logrocket 示例,包括会话回放、错误跟踪、性能监控和用户行为分析

💻 LogRocket Hello World typescript

🟢 simple

基础 Logrocket 设置,包含会话记录和初始化

⏱️ 10 min 🏷️ frontend, monitoring, analytics
Prerequisites: JavaScript basics, HTML/CSS, Browser APIs
// LogRocket Hello World - Basic Frontend Monitoring
// Simple session recording and error tracking setup

import LogRocket from 'logrocket'

// Initialize LogRocket with your application ID
LogRocket.init('your-app-id')

// Record basic user information
LogRocket.identify('user-123', {
  name: 'John Doe',
  email: '[email protected]',
  subscriptionType: 'premium'
})

// Track custom events
LogRocket.track('User signed up', {
  method: 'email',
  timestamp: new Date().toISOString()
})

// Console.log for debugging
console.log('LogRocket initialized and recording!')

// Handle errors automatically
window.addEventListener('error', (event) => {
  LogRocket.captureException(event.error)
})

// Track page navigation
window.addEventListener('popstate', () => {
  LogRocket.track('Page navigation', {
    url: window.location.href,
    timestamp: new Date().toISOString()
  })
})

💻 高级会话回放 typescript

🟡 intermediate ⭐⭐⭐

可配置的会话记录,包含自定义事件和过滤器

⏱️ 25 min 🏷️ frontend, monitoring, analytics, performance
Prerequisites: LogRocket basics, DOM manipulation, Performance APIs, Event handling
// LogRocket Advanced Session Replay
// Comprehensive session recording with custom event tracking

import LogRocket from 'logrocket'
import LogRocketFuzzySearch from 'logrocket-fuzzy-search'

// Initialize with advanced configuration
LogRocket.init('your-app-id', {
  network: {
    requestSanitizer: (request) => {
      // Remove sensitive headers
      if (request.headers.authorization) {
        delete request.headers.authorization
      }
      if (request.body && request.body.password) {
        request.body.password = '***'
      }
      return request
    },
    responseSanitizer: (response) => {
      // Sanitize sensitive response data
      if (response.body && response.body.token) {
        response.body.token = '***'
      }
      return response
    }
  },
  console: {
    shouldAggregateConsoleLogs: true
  },
  dom: {
    baseHref: window.location.origin,
    inputSanitizer: true
  }
})

// Enable fuzzy search for text inputs
LogRocketFuzzySearch(LogRocket)

// Track form interactions
class FormTracker {
  constructor() {
    this.setupFormListeners()
  }

  private setupFormListeners() {
    // Track form submissions
    document.addEventListener('submit', (event) => {
      const form = event.target as HTMLFormElement
      LogRocket.track('Form submitted', {
        formId: form.id,
        formName: form.name,
        fields: this.getFormData(form)
      })
    })

    // Track field focus events
    document.addEventListener('focusin', (event) => {
      const element = event.target as HTMLElement
      if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
        LogRocket.track('Field focused', {
          fieldName: element.getAttribute('name'),
          fieldType: element.getAttribute('type'),
          fieldId: element.id
        })
      }
    })

    // Track validation errors
    document.addEventListener('invalid', (event) => {
      const element = event.target as HTMLInputElement
      LogRocket.track('Validation error', {
        fieldName: element.name,
        fieldType: element.type,
        validationMessage: element.validationMessage
      })
    })
  }

  private getFormData(form: HTMLFormElement): Record<string, any> {
    const formData = new FormData(form)
    const data: Record<string, any> = {}

    // Sanitize sensitive data
    for (const [key, value] of formData.entries()) {
      if (key.toLowerCase().includes('password') ||
          key.toLowerCase().includes('token') ||
          key.toLowerCase().includes('secret')) {
        data[key] = '***'
      } else {
        data[key] = value
      }
    }

    return data
  }
}

// Track user interactions with specific elements
class InteractionTracker {
  constructor() {
    this.setupClickTracking()
    this.setupScrollTracking()
    this.setupRageClickDetection()
  }

  private setupClickTracking() {
    document.addEventListener('click', (event) => {
      const element = event.target as HTMLElement
      const elementInfo = {
        tagName: element.tagName,
        className: element.className,
        id: element.id,
        textContent: element.textContent?.slice(0, 50),
        attributes: this.getElementAttributes(element)
      }

      LogRocket.track('Element clicked', elementInfo)
    })
  }

  private setupScrollTracking() {
    let lastScrollTop = 0
    let scrollTimeout: number

    window.addEventListener('scroll', () => {
      clearTimeout(scrollTimeout)
      scrollTimeout = window.setTimeout(() => {
        const scrollTop = window.pageYOffset || document.documentElement.scrollTop
        const scrollDirection = scrollTop > lastScrollTop ? 'down' : 'up'
        const scrollPercentage = Math.round(
          (scrollTop / (document.documentElement.scrollHeight - window.innerHeight)) * 100
        )

        LogRocket.track('Page scrolled', {
          direction: scrollDirection,
          percentage: scrollPercentage,
          scrollTop
        })

        lastScrollTop = scrollTop
      }, 100)
    })
  }

  private setupRageClickDetection() {
    const clicks: Array<{ time: number; x: number; y: number }> = []
    const RAGE_CLICK_THRESHOLD = 3
    const RAGE_CLICK_TIMEFRAME = 1000 // 1 second

    document.addEventListener('click', (event) => {
      const now = Date.now()
      const click = { time: now, x: event.clientX, y: event.clientY }

      // Remove old clicks
      clicks.filter(c => now - c.time < RAGE_CLICK_TIMEFRAME)
      clicks.push(click)

      if (clicks.length >= RAGE_CLICK_THRESHOLD) {
        LogRocket.track('Rage click detected', {
          clickCount: clicks.length,
          timeframe: RAGE_CLICK_TIMEFRAME,
          x: event.clientX,
          y: event.clientY,
          target: (event.target as HTMLElement).tagName
        })
      }
    })
  }

  private getElementAttributes(element: HTMLElement): Record<string, string> {
    const attrs: Record<string, string> = {}
    for (const attr of element.attributes) {
      if (!['id', 'class', 'style'].includes(attr.name)) {
        attrs[attr.name] = attr.value
      }
    }
    return attrs
  }
}

// Initialize trackers
const formTracker = new FormTracker()
const interactionTracker = new InteractionTracker()

// Track performance metrics
class PerformanceTracker {
  constructor() {
    this.trackPageLoad()
    this.trackResourceTiming()
  }

  private trackPageLoad() {
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming

      LogRocket.track('Page load completed', {
        loadTime: navigation.loadEventEnd - navigation.fetchStart,
        domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
        firstPaint: this.getFirstPaint(),
        firstContentfulPaint: this.getFirstContentfulPaint()
      })
    })
  }

  private trackResourceTiming() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'resource') {
          LogRocket.track('Resource loaded', {
            name: entry.name,
            duration: entry.duration,
            size: (entry as PerformanceResourceTiming).transferSize,
            type: this.getResourceType(entry.name)
          })
        }
      }
    })

    observer.observe({ entryTypes: ['resource'] })
  }

  private getFirstPaint(): number {
    const paintEntries = performance.getEntriesByType('paint')
    const firstPaint = paintEntries.find(entry => entry.name === 'first-paint')
    return firstPaint ? firstPaint.startTime : 0
  }

  private getFirstContentfulPaint(): number {
    const paintEntries = performance.getEntriesByType('paint')
    const fcp = paintEntries.find(entry => entry.name === 'first-contentful-paint')
    return fcp ? fcp.startTime : 0
  }

  private getResourceType(url: string): string {
    if (url.match(/\.(css)$/)) return 'css'
    if (url.match(/\.(js)$/)) return 'javascript'
    if (url.match(/\.(png|jpg|jpeg|gif|webp|svg)$/)) return 'image'
    if (url.match(/\.(woff|woff2|ttf|eot)$/)) return 'font'
    return 'other'
  }
}

// Initialize performance tracker
const performanceTracker = new PerformanceTracker()

console.log('LogRocket advanced session replay initialized')

// Export for React usage
export { FormTracker, InteractionTracker, PerformanceTracker }

💻 React 错误边界集成 typescript

🟡 intermediate ⭐⭐⭐

React 错误边界与 Logrocket 集成,实现全面错误跟踪

⏱️ 20 min 🏷️ react, frontend, error-handling, monitoring
Prerequisites: React basics, JavaScript error handling, LogRocket
// LogRocket React Error Boundary Integration
// Automatic error capturing and reporting in React applications

import React, { Component, ErrorInfo, ReactNode } from 'react'
import LogRocket from 'logrocket'

interface Props {
  children: ReactNode
  fallback?: ReactNode
  onError?: (error: Error, errorInfo: ErrorInfo) => void
}

interface State {
  hasError: boolean
  error?: Error
  errorInfo?: ErrorInfo
  retryCount: number
}

class LogRocketErrorBoundary extends Component<Props, State> {
  private maxRetries = 3

  constructor(props: Props) {
    super(props)
    this.state = {
      hasError: false,
      retryCount: 0
    }
  }

  static getDerivedStateFromError(error: Error): Partial<State> {
    return {
      hasError: true,
      error
    }
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.setState({ errorInfo })

    // Log to LogRocket
    LogRocket.captureException(error, {
      tags: {
        component: 'ErrorBoundary',
        severity: 'critical'
      },
      extra: {
        errorInfo,
        componentStack: errorInfo.componentStack,
        retryCount: this.state.retryCount
      }
    })

    // Track error event
    LogRocket.track('React error caught', {
      errorMessage: error.message,
      errorName: error.name,
      componentStack: errorInfo.componentStack,
      retryCount: this.state.retryCount,
      userAgent: navigator.userAgent,
      url: window.location.href
    })

    // Call custom error handler
    if (this.props.onError) {
      this.props.onError(error, errorInfo)
    }
  }

  handleRetry = () => {
    if (this.state.retryCount < this.maxRetries) {
      this.setState(prevState => ({
        hasError: false,
        error: undefined,
        errorInfo: undefined,
        retryCount: prevState.retryCount + 1
      }))

      LogRocket.track('Error boundary retry', {
        retryCount: this.state.retryCount + 1,
        maxRetries: this.maxRetries
      })
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div style={{
          padding: '20px',
          border: '1px solid #ff6b6b',
          borderRadius: '8px',
          backgroundColor: '#ffe0e0',
          margin: '20px'
        }}>
          <h2 style={{ color: '#d63031' }}>Something went wrong</h2>
          <p>We're sorry, but something unexpected happened.</p>

          {this.state.retryCount < this.maxRetries ? (
            <button
              onClick={this.handleRetry}
              style={{
                padding: '10px 20px',
                backgroundColor: '#0984e3',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                cursor: 'pointer',
                marginRight: '10px'
              }}
            >
              Try Again ({this.maxRetries - this.state.retryCount} attempts left)
            </button>
          ) : (
            <p>Please refresh the page or contact support if the problem persists.</p>
          )}

          <details style={{ marginTop: '20px' }}>
            <summary>Error Details</summary>
            <pre style={{
              fontSize: '12px',
              backgroundColor: '#f8f9fa',
              padding: '10px',
              overflow: 'auto',
              maxHeight: '200px'
            }}>
              {this.state.error?.stack}
            </pre>
          </details>
        </div>
      )
    }

    return this.props.children
  }
}

// Hook for functional components
function useLogRocketErrorBoundary() {
  const [error, setError] = React.useState<Error | null>(null)

  const captureError = React.useCallback((error: Error) => {
    setError(error)

    LogRocket.captureException(error, {
      tags: {
        component: 'useLogRocketErrorBoundary',
        severity: 'critical'
      }
    })

    LogRocket.track('Functional component error', {
      errorMessage: error.message,
      errorName: error.name,
      stack: error.stack,
      url: window.location.href
    })
  }, [])

  const resetError = React.useCallback(() => {
    setError(null)
    LogRocket.track('Error boundary reset')
  }, [])

  React.useEffect(() => {
    const handleError = (event: ErrorEvent) => {
      captureError(event.error)
    }

    const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
      captureError(new Error(event.reason))
    }

    window.addEventListener('error', handleError)
    window.addEventListener('unhandledrejection', handleUnhandledRejection)

    return () => {
      window.removeEventListener('error', handleError)
      window.removeEventListener('unhandledrejection', handleUnhandledRejection)
    }
  }, [captureError])

  return { error, captureError, resetError }
}

// Higher-order component for automatic error boundary
function withLogRocketErrorBoundary<P extends object>(
  Component: React.ComponentType<P>,
  options?: {
    fallback?: ReactNode
    onError?: (error: Error, errorInfo: ErrorInfo) => void
  }
) {
  return function WithErrorBoundary(props: P) {
    return (
      <LogRocketErrorBoundary
        fallback={options?.fallback}
        onError={options?.onError}
      >
        <Component {...props} />
      </LogRocketErrorBoundary>
    )
  }
}

// Usage example
const MyComponent = () => {
  const [shouldThrow, setShouldThrow] = React.useState(false)

  if (shouldThrow) {
    throw new Error('This is a test error!')
  }

  return (
    <div>
      <h1>My Component</h1>
      <button onClick={() => setShouldThrow(true)}>
        Throw Error
      </button>
    </div>
  )
}

// Wrap component with error boundary
const SafeComponent = withLogRocketErrorBoundary(MyComponent, {
  onError: (error, errorInfo) => {
    console.error('Custom error handler:', error, errorInfo)
  }
})

// Functional component with hook
const FunctionalComponent = () => {
  const { error, captureError, resetError } = useLogRocketErrorBoundary()

  const handleClick = () => {
    try {
      throw new Error('User triggered error!')
    } catch (err) {
      captureError(err as Error)
    }
  }

  if (error) {
    return (
      <div>
        <p>Error occurred: {error.message}</p>
        <button onClick={resetError}>Reset Error</button>
      </div>
    )
  }

  return <button onClick={handleClick}>Trigger Error</button>
}

export {
  LogRocketErrorBoundary,
  useLogRocketErrorBoundary,
  withLogRocketErrorBoundary,
  SafeComponent,
  FunctionalComponent
}

💻 自定义分析仪表板 typescript

🔴 complex ⭐⭐⭐⭐

使用 Logrocket 数据和见解构建自定义分析仪表板

⏱️ 45 min 🏷️ frontend, analytics, dashboard, monitoring
Prerequisites: React hooks, LogRocket API, Data visualization, State management
// LogRocket Custom Analytics Dashboard
// Real-time analytics dashboard using LogRocket session data

import React, { useState, useEffect, useCallback } from 'react'
import LogRocket from 'logrocket'

interface SessionData {
  id: string
  timestamp: number
  duration: number
  userId: string
  events: Array<{
    type: string
    timestamp: number
    data: any
  }>
  errors: Array<{
    message: string
    stack: string
    timestamp: number
  }>
  pageViews: Array<{
    url: string
    timestamp: number
    duration: number
  }>
}

interface AnalyticsMetrics {
  totalSessions: number
  averageSessionDuration: number
  errorRate: number
  topPages: Array<{ url: string; views: number }>
  commonErrors: Array<{ message: string; count: number }>
  userRetention: {
    day1: number
    day7: number
    day30: number
  }
}

const LogRocketAnalyticsDashboard: React.FC = () => {
  const [sessions, setSessions] = useState<SessionData[]>([])
  const [metrics, setMetrics] = useState<AnalyticsMetrics | null>(null)
  const [loading, setLoading] = useState(true)
  const [dateRange, setDateRange] = useState({
    start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
    end: new Date()
  })
  const [selectedSession, setSelectedSession] = useState<SessionData | null>(null)

  // Initialize LogRocket session tracking
  useEffect(() => {
    LogRocket.getSessionURL((sessionURL) => {
      if (sessionURL) {
        LogRocket.track('Analytics dashboard viewed', {
          sessionURL,
          timestamp: Date.now()
        })
      }
    })
  }, [])

  // Fetch session data from LogRocket API
  const fetchSessionData = useCallback(async () => {
    try {
      setLoading(true)

      // This would typically call your backend API that queries LogRocket
      // For demo purposes, we'll simulate the data structure
      const mockSessionData: SessionData[] = [
        {
          id: 'session-1',
          timestamp: Date.now() - 3600000,
          duration: 1800000, // 30 minutes
          userId: 'user-123',
          events: [
            { type: 'page_view', timestamp: Date.now() - 3600000, data: { url: '/home' } },
            { type: 'click', timestamp: Date.now() - 3500000, data: { element: 'button' } },
            { type: 'form_submit', timestamp: Date.now() - 3400000, data: { formId: 'contact' } }
          ],
          errors: [
            { message: 'TypeError: Cannot read property of undefined', stack: '...', timestamp: Date.now() - 3300000 }
          ],
          pageViews: [
            { url: '/home', timestamp: Date.now() - 3600000, duration: 600000 },
            { url: '/products', timestamp: Date.now() - 3000000, duration: 1200000 }
          ]
        }
      ]

      setSessions(mockSessionData)
      calculateMetrics(mockSessionData)
    } catch (error) {
      console.error('Failed to fetch session data:', error)
      LogRocket.captureException(error as Error)
    } finally {
      setLoading(false)
    }
  }, [dateRange])

  // Calculate analytics metrics
  const calculateMetrics = (sessionData: SessionData[]) => {
    const metrics: AnalyticsMetrics = {
      totalSessions: sessionData.length,
      averageSessionDuration: sessionData.reduce((acc, s) => acc + s.duration, 0) / sessionData.length,
      errorRate: sessionData.filter(s => s.errors.length > 0).length / sessionData.length,
      topPages: getTopPages(sessionData),
      commonErrors: getCommonErrors(sessionData),
      userRetention: calculateUserRetention(sessionData)
    }

    setMetrics(metrics)

    // Track metrics calculation event
    LogRocket.track('Analytics metrics calculated', {
      totalSessions: metrics.totalSessions,
      errorRate: metrics.errorRate,
      averageSessionDuration: metrics.averageSessionDuration
    })
  }

  // Get most viewed pages
  const getTopPages = (sessionData: SessionData[]) => {
    const pageViews: Record<string, number> = {}

    sessionData.forEach(session => {
      session.pageViews.forEach(view => {
        pageViews[view.url] = (pageViews[view.url] || 0) + 1
      })
    })

    return Object.entries(pageViews)
      .sort(([, a], [, b]) => b - a)
      .slice(0, 10)
      .map(([url, views]) => ({ url, views }))
  }

  // Get most common errors
  const getCommonErrors = (sessionData: SessionData[]) => {
    const errors: Record<string, number> = {}

    sessionData.forEach(session => {
      session.errors.forEach(error => {
        errors[error.message] = (errors[error.message] || 0) + 1
      })
    })

    return Object.entries(errors)
      .sort(([, a], [, b]) => b - a)
      .slice(0, 5)
      .map(([message, count]) => ({ message, count }))
  }

  // Calculate user retention metrics
  const calculateUserRetention = (sessionData: SessionData[]) => {
    // Simplified retention calculation
    const now = Date.now()
    const dayMs = 24 * 60 * 60 * 1000

    return {
      day1: Math.random() * 0.5 + 0.3, // 30-80%
      day7: Math.random() * 0.3 + 0.1, // 10-40%
      day30: Math.random() * 0.2 + 0.05 // 5-25%
    }
  }

  // Filter sessions by date range
  const getFilteredSessions = () => {
    return sessions.filter(session => {
      const sessionDate = new Date(session.timestamp)
      return sessionDate >= dateRange.start && sessionDate <= dateRange.end
    })
  }

  // Export session data
  const exportData = () => {
    const filteredSessions = getFilteredSessions()
    const dataStr = JSON.stringify(filteredSessions, null, 2)
    const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr)

    const exportFileDefaultName = `logrocket-sessions-${new Date().toISOString().split('T')[0]}.json`

    const linkElement = document.createElement('a')
    linkElement.setAttribute('href', dataUri)
    linkElement.setAttribute('download', exportFileDefaultName)
    linkElement.click()

    LogRocket.track('Analytics data exported', {
      sessionCount: filteredSessions.length,
      dateRange
    })
  }

  useEffect(() => {
    fetchSessionData()
  }, [fetchSessionData])

  if (loading) {
    return <div>Loading analytics data...</div>
  }

  if (!metrics) {
    return <div>No data available</div>
  }

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
      <h1>LogRocket Analytics Dashboard</h1>

      {/* Date Range Selector */}
      <div style={{ marginBottom: '20px', display: 'flex', gap: '10px', alignItems: 'center' }}>
        <label>Date Range:</label>
        <input
          type="date"
          value={dateRange.start.toISOString().split('T')[0]}
          onChange={(e) => setDateRange(prev => ({ ...prev, start: new Date(e.target.value) }))}
        />
        <span>to</span>
        <input
          type="date"
          value={dateRange.end.toISOString().split('T')[0]}
          onChange={(e) => setDateRange(prev => ({ ...prev, end: new Date(e.target.value) }))}
        />
        <button onClick={fetchSessionData}>Refresh</button>
        <button onClick={exportData}>Export Data</button>
      </div>

      {/* Key Metrics */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '20px', marginBottom: '30px' }}>
        <div style={{ padding: '15px', border: '1px solid #ddd', borderRadius: '8px' }}>
          <h3>Total Sessions</h3>
          <p style={{ fontSize: '24px', fontWeight: 'bold', color: '#0984e3' }}>{metrics.totalSessions}</p>
        </div>

        <div style={{ padding: '15px', border: '1px solid #ddd', borderRadius: '8px' }}>
          <h3>Avg Session Duration</h3>
          <p style={{ fontSize: '24px', fontWeight: 'bold', color: '#00b894' }}>
            {Math.round(metrics.averageSessionDuration / 1000 / 60)}m
          </p>
        </div>

        <div style={{ padding: '15px', border: '1px solid #ddd', borderRadius: '8px' }}>
          <h3>Error Rate</h3>
          <p style={{ fontSize: '24px', fontWeight: 'bold', color: '#d63031' }}>
            {(metrics.errorRate * 100).toFixed(1)}%
          </p>
        </div>

        <div style={{ padding: '15px', border: '1px solid #ddd', borderRadius: '8px' }}>
          <h3>Day 1 Retention</h3>
          <p style={{ fontSize: '24px', fontWeight: 'bold', color: '#fdcb6e' }}>
            {(metrics.userRetention.day1 * 100).toFixed(0)}%
          </p>
        </div>
      </div>

      {/* Top Pages */}
      <div style={{ marginBottom: '30px' }}>
        <h2>Top Pages</h2>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr style={{ backgroundColor: '#f8f9fa' }}>
              <th style={{ padding: '10px', textAlign: 'left' }}>Page URL</th>
              <th style={{ padding: '10px', textAlign: 'left' }}>Views</th>
            </tr>
          </thead>
          <tbody>
            {metrics.topPages.map((page, index) => (
              <tr key={index}>
                <td style={{ padding: '10px', borderTop: '1px solid #ddd' }}>{page.url}</td>
                <td style={{ padding: '10px', borderTop: '1px solid #ddd' }}>{page.views}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Common Errors */}
      <div style={{ marginBottom: '30px' }}>
        <h2>Common Errors</h2>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr style={{ backgroundColor: '#f8f9fa' }}>
              <th style={{ padding: '10px', textAlign: 'left' }}>Error Message</th>
              <th style={{ padding: '10px', textAlign: 'left' }}>Count</th>
            </tr>
          </thead>
          <tbody>
            {metrics.commonErrors.map((error, index) => (
              <tr key={index}>
                <td style={{ padding: '10px', borderTop: '1px solid #ddd' }}>{error.message}</td>
                <td style={{ padding: '10px', borderTop: '1px solid #ddd' }}>{error.count}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Sessions List */}
      <div>
        <h2>Recent Sessions</h2>
        <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
          {getFilteredSessions().map((session) => (
            <div
              key={session.id}
              style={{
                padding: '10px',
                border: '1px solid #ddd',
                borderRadius: '4px',
                marginBottom: '10px',
                cursor: 'pointer'
              }}
              onClick={() => setSelectedSession(session)}
            >
              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                <span><strong>Session:</strong> {session.id}</span>
                <span>{new Date(session.timestamp).toLocaleString()}</span>
              </div>
              <div>
                <small>Duration: {Math.round(session.duration / 1000 / 60)}m | Errors: {session.errors.length}</small>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* Session Details Modal */}
      {selectedSession && (
        <div style={{
          position: 'fixed',
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          backgroundColor: 'rgba(0,0,0,0.5)',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          zIndex: 1000
        }}>
          <div style={{
            backgroundColor: 'white',
            padding: '20px',
            borderRadius: '8px',
            maxWidth: '600px',
            maxHeight: '80vh',
            overflowY: 'auto'
          }}>
            <h2>Session Details</h2>
            <p><strong>Session ID:</strong> {selectedSession.id}</p>
            <p><strong>User ID:</strong> {selectedSession.userId}</p>
            <p><strong>Duration:</strong> {Math.round(selectedSession.duration / 1000 / 60)} minutes</p>

            <h3>Events</h3>
            <div style={{ maxHeight: '200px', overflowY: 'auto' }}>
              {selectedSession.events.map((event, index) => (
                <div key={index} style={{ padding: '5px', borderBottom: '1px solid #eee' }}>
                  <small>{event.type} - {new Date(event.timestamp).toLocaleTimeString()}</small>
                </div>
              ))}
            </div>

            <button
              onClick={() => setSelectedSession(null)}
              style={{ marginTop: '20px', padding: '10px 20px', cursor: 'pointer' }}
            >
              Close
            </button>
          </div>
        </div>
      )}
    </div>
  )
}

export default LogRocketAnalyticsDashboard