Vibe coding with AI course · Module 12: Medium Clone

Performance Monitoring - Sentry/Analytics

1 min read
In this lesson2

Monitoring errors and performance in production.

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;
  }
}

See you soon!

Code for this lesson: 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: 'Captain Redbeard',
      avatar: 'https://i.pravatar.cc/150?img=12'
    },
    content: 'Great article! Really enjoyed reading about this topic. The examples were particularly helpful.',
    claps: 24,
    createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
    replies: [
      {
        id: '2',
        author: {
          name: 'Sailor Sam',
          avatar: 'https://i.pravatar.cc/150?img=33'
        },
        content: 'I agree! The practical examples made it easy to understand.',
        claps: 8,
        createdAt: new Date(Date.now() - 1000 * 60 * 30),
        replies: []
      }
    ]
  },
  {
    id: '3',
    author: {
      name: 'Navigator Nancy',
      avatar: 'https://i.pravatar.cc/150?img=45'
    },
    content: 'Would love to see more content like this. Thanks for sharing!',
    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">2h ago</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>Applause</span>
              <span>{response.claps}</span>
            </button>
            <button className="hover:text-gray-700">Reply</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(`Response posted: ${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">
          Responses ({responses.length})
        </h2>

        {/* Write Response */}
        <div className="mb-8 border border-gray-200 rounded-lg p-4">
          <textarea
            value={content}
            onChange={(e) => setContent(e.target.value)}
            placeholder="What are your thoughts?"
            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"
            >
              Respond
            </button>
          </div>
        </div>

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

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is Sentry used for in a Next.js app?

  2. 2. Which Error Boundary method is used to report the error to Sentry?

Hands-on tasks in the game

  • Code editor

    Error Boundary with Sentry reporting

  • Vertical ordering

    Arrange the steps of handling an error with an Error Boundary:

Useful articles