Kurs vibe coding z AI · Moduł 12: Medium Clone

Performance Monitoring - Sentry/Analytics

1 min czytania
W tej lekcji2

Monitorowanie błędów i wydajności w produkcji.

Sentry Setup

npm install @sentry/nextjs
// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
  environment: process.env.NODE_ENV
});

Error Boundaries

'use client';

import { Component, ReactNode } from 'react';
import * as Sentry from '@sentry/nextjs';

class ErrorBoundary extends Component<
  { children: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: any) {
    Sentry.captureException(error, { contexts: { react: errorInfo } });
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong</div>;
    }
    return this.props.children;
  }
}

Do zobaczenia!

Kod do tej lekcji: App.tsx
'use client';

import { useState } from 'react';

interface Response {
  id: string;
  author: {
    name: string;
    avatar: string;
  };
  content: string;
  claps: number;
  createdAt: Date;
  replies: Response[];
}

const mockResponses: Response[] = [
  {
    id: '1',
    author: {
      name: 'Kapitan Redbeard',
      avatar: 'https://i.pravatar.cc/150?img=12'
    },
    content: 'Świetny artykuł! Czytało się go z prawdziwą przyjemnością. Szczególnie pomogły mi przykłady.',
    claps: 24,
    createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
    replies: [
      {
        id: '2',
        author: {
          name: 'Marynarz Sam',
          avatar: 'https://i.pravatar.cc/150?img=33'
        },
        content: 'Zgadzam się! Dzięki praktycznym przykładom łatwo to zrozumieć.',
        claps: 8,
        createdAt: new Date(Date.now() - 1000 * 60 * 30),
        replies: []
      }
    ]
  },
  {
    id: '3',
    author: {
      name: 'Nawigatorka Nancy',
      avatar: 'https://i.pravatar.cc/150?img=45'
    },
    content: 'Chętnie przeczytam więcej takich tekstów. Dzięki, że się dzielisz!',
    claps: 15,
    createdAt: new Date(Date.now() - 1000 * 60 * 60),
    replies: []
  }
];

function ResponseItem({ response }: { response: Response }) {
  return (
    <div className="border-b border-gray-200 pb-6">
      <div className="flex items-start gap-3">
        <img
          src={response.author.avatar}
          alt={response.author.name}
          className="w-10 h-10 rounded-full"
        />
        <div className="flex-1">
          <div className="flex items-center gap-2 mb-2">
            <span className="font-semibold">{response.author.name}</span>
            <span className="text-sm text-gray-500">2 godz. temu</span>
          </div>
          <p className="text-gray-800 leading-relaxed mb-3">
            {response.content}
          </p>
          <div className="flex items-center gap-4 text-sm text-gray-500">
            <button className="flex items-center gap-1 hover:text-green-600">
              <span>Brawo</span>
              <span>{response.claps}</span>
            </button>
            <button className="hover:text-gray-700">Odpowiedz</button>
          </div>

          {response.replies.length > 0 && (
            <div className="mt-4 pl-6 border-l-2 border-gray-100 space-y-4">
              {response.replies.map(reply => (
                <ResponseItem key={reply.id} response={reply} />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

export default function ResponseSection() {
  const [content, setContent] = useState('');
  const [responses, setResponses] = useState(mockResponses);

  const handleSubmit = () => {
    if (!content.trim()) return;

    alert(`Opublikowano odpowiedź: ${content}`);
    setContent('');
  };

  return (
    <div className="min-h-screen bg-white">
      <div className="max-w-2xl mx-auto px-8 py-12">
        <h2 className="text-2xl font-bold mb-6">
          Odpowiedzi ({responses.length})
        </h2>

        {/* Napisz odpowiedź */}
        <div className="mb-8 border border-gray-200 rounded-lg p-4">
          <textarea
            value={content}
            onChange={(e) => setContent(e.target.value)}
            placeholder="Co o tym myślisz?"
            className="w-full outline-none resize-none text-lg"
            rows={3}
          />
          <div className="flex justify-end mt-2">
            <button
              onClick={handleSubmit}
              disabled={!content.trim()}
              className="px-6 py-2 bg-green-600 text-white rounded-full font-semibold hover:bg-green-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
            >
              Odpowiedz
            </button>
          </div>
        </div>

        {/* Odpowiedzi */}
        <div className="space-y-6">
          {responses.map(response => (
            <ResponseItem key={response.id} response={response} />
          ))}
        </div>
      </div>
    </div>
  );
}

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służy Sentry w aplikacji Next.js?

  2. 2. Która metoda Error Boundary służy do zaraportowania błędu do Sentry?

Zadania praktyczne w grze

  • Edytor kodu

    Error Boundary z raportowaniem do Sentry

  • Układanie w pionie

    Uporządkuj kroki obsługi błędu przez Error Boundary:

Przydatne artykuły