Skip to main content
Glama

Sentry MCP

Official
by getsentry
search-events.test.ts13.6 kB
import { describe, it, expect, vi, beforeEach } from "vitest"; import { http, HttpResponse } from "msw"; import { mswServer } from "@sentry/mcp-server-mocks"; import searchEvents from "./search-events"; import { generateText } from "ai"; import { UserInputError } from "../errors"; // Mock the AI SDK vi.mock("@ai-sdk/openai", () => { const mockModel = vi.fn(() => "mocked-model"); return { openai: mockModel, createOpenAI: vi.fn(() => mockModel), }; }); vi.mock("ai", () => ({ generateText: vi.fn(), tool: vi.fn(() => ({ execute: vi.fn() })), Output: { object: vi.fn(() => ({})) }, })); describe("search_events", () => { const mockGenerateText = vi.mocked(generateText); // Helper to create AI response for different datasets const mockAIResponse = ( dataset: "errors" | "logs" | "spans", query = "test query", fields?: string[], errorMessage?: string, sort?: string, timeRange?: { statsPeriod: string } | { start: string; end: string }, ) => { const defaultFields = { errors: ["issue", "title", "project", "timestamp", "level", "message"], logs: ["timestamp", "project", "message", "severity", "trace"], spans: [ "span.op", "span.description", "span.duration", "transaction", "timestamp", "project", ], }; const defaultSorts = { errors: "-timestamp", logs: "-timestamp", spans: "-span.duration", }; const output = errorMessage ? { error: errorMessage } : { dataset, query, fields: fields || defaultFields[dataset], sort: sort || defaultSorts[dataset], ...(timeRange && { timeRange }), }; return { text: JSON.stringify(output), experimental_output: output, finishReason: "stop" as const, usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 }, warnings: [] as const, } as any; }; beforeEach(() => { vi.clearAllMocks(); process.env.OPENAI_API_KEY = "test-key"; mockGenerateText.mockResolvedValue(mockAIResponse("errors")); }); it("should handle spans dataset queries", async () => { // Mock AI response for spans dataset mockGenerateText.mockResolvedValueOnce( mockAIResponse("spans", 'span.op:"db.query"', [ "span.op", "span.description", "span.duration", ]), ); // Mock the Sentry API response mswServer.use( http.get( "https://sentry.io/api/0/organizations/test-org/events/", ({ request }) => { const url = new URL(request.url); expect(url.searchParams.get("dataset")).toBe("spans"); return HttpResponse.json({ data: [ { id: "span1", "span.op": "db.query", "span.description": "SELECT * FROM users", "span.duration": 1500, }, ], }); }, ), ); const result = await searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "database queries", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ); expect(mockGenerateText).toHaveBeenCalled(); expect(result).toContain("span1"); expect(result).toContain("db.query"); }); it("should handle errors dataset queries", async () => { // Mock AI response for errors dataset mockGenerateText.mockResolvedValueOnce( mockAIResponse("errors", "level:error", [ "issue", "title", "level", "timestamp", ]), ); // Mock the Sentry API response mswServer.use( http.get( "https://sentry.io/api/0/organizations/test-org/events/", ({ request }) => { const url = new URL(request.url); expect(url.searchParams.get("dataset")).toBe("errors"); return HttpResponse.json({ data: [ { id: "error1", issue: "PROJ-123", title: "Database Connection Error", level: "error", timestamp: "2024-01-15T10:30:00Z", }, ], }); }, ), ); const result = await searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "database errors", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ); expect(mockGenerateText).toHaveBeenCalled(); expect(result).toContain("Database Connection Error"); expect(result).toContain("PROJ-123"); }); it("should handle logs dataset queries", async () => { // Mock AI response for logs dataset mockGenerateText.mockResolvedValueOnce( mockAIResponse("logs", "severity:error", [ "timestamp", "message", "severity", ]), ); // Mock the Sentry API response mswServer.use( http.get( "https://sentry.io/api/0/organizations/test-org/events/", ({ request }) => { const url = new URL(request.url); expect(url.searchParams.get("dataset")).toBe("ourlogs"); // API converts logs -> ourlogs return HttpResponse.json({ data: [ { id: "log1", timestamp: "2024-01-15T10:30:00Z", message: "Connection failed to database", severity: "error", }, ], }); }, ), ); const result = await searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "error logs", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ); expect(mockGenerateText).toHaveBeenCalled(); expect(result).toContain("Connection failed to database"); expect(result).toContain("🔴 [ERROR]"); }); it("should handle AI agent errors gracefully", async () => { // Mock AI response with error mockGenerateText.mockResolvedValueOnce( mockAIResponse("errors", "", [], "Cannot parse this query"), ); await expect( searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "some impossible query !@#$%", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ), ).rejects.toThrow(UserInputError); }); it("should return UserInputError for time series queries", async () => { // Mock AI response with time series error mockGenerateText.mockResolvedValueOnce( mockAIResponse( "errors", "", [], "Time series aggregations are not currently supported.", ), ); const promise = searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "show me errors over time", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ); // Check that it throws UserInputError await expect(promise).rejects.toThrow(UserInputError); // Check that the error message contains the expected text await expect(promise).rejects.toThrow( "Time series aggregations are not currently supported", ); }); it("should handle API errors gracefully", async () => { // Mock successful AI response mockGenerateText.mockResolvedValueOnce( mockAIResponse("errors", "level:error"), ); // Mock API error mswServer.use( http.get("https://sentry.io/api/0/organizations/test-org/events/", () => HttpResponse.json( { detail: "Organization not found" }, { status: 404 }, ), ), ); await expect( searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "any query", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ), ).rejects.toThrow(); }); it("should handle missing sort parameter", async () => { // Mock AI response missing sort parameter mockGenerateText.mockResolvedValueOnce({ text: JSON.stringify({ dataset: "errors", query: "test", fields: ["title"], }), experimental_output: { dataset: "errors", query: "test", fields: ["title"], }, } as any); await expect( searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "any query", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ), ).rejects.toThrow("missing required 'sort' parameter"); }); it("should handle agent self-correction when sort field not in fields array", async () => { // First call: Agent returns sort field not in fields (will fail validation) // Second call: Agent self-corrects by adding sort field to fields array mockGenerateText.mockResolvedValueOnce({ text: JSON.stringify({ dataset: "errors", query: "test", fields: ["title", "timestamp"], // Added timestamp after self-correction sort: "-timestamp", }), experimental_output: { dataset: "errors", query: "test", fields: ["title", "timestamp"], sort: "-timestamp", explanation: "Self-corrected to include sort field in fields array", }, } as any); // Mock the Sentry API response mswServer.use( http.get("https://sentry.io/api/0/organizations/test-org/events/", () => { return HttpResponse.json({ data: [ { id: "error1", title: "Test Error", timestamp: "2024-01-15T10:30:00Z", }, ], }); }), ); const result = await searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "recent errors", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ); // Verify the agent was called and result contains the data expect(mockGenerateText).toHaveBeenCalled(); expect(result).toContain("Test Error"); }); it("should correctly handle user agent queries", async () => { // Mock AI response for user agent query in spans dataset mockGenerateText.mockResolvedValueOnce( mockAIResponse( "spans", "has:mcp.tool.name AND has:user_agent.original", ["user_agent.original", "count()"], undefined, "-count()", { statsPeriod: "24h" }, ), ); // Mock the Sentry API response mswServer.use( http.get( "https://sentry.io/api/0/organizations/test-org/events/", ({ request }) => { const url = new URL(request.url); expect(url.searchParams.get("dataset")).toBe("spans"); expect(url.searchParams.get("query")).toBe( "has:mcp.tool.name AND has:user_agent.original", ); expect(url.searchParams.get("sort")).toBe("-count"); // API transforms count() to count expect(url.searchParams.get("statsPeriod")).toBe("24h"); // Verify it's using user_agent.original, not user.id expect(url.searchParams.getAll("field")).toContain( "user_agent.original", ); expect(url.searchParams.getAll("field")).toContain("count()"); return HttpResponse.json({ data: [ { "user_agent.original": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "count()": 150, }, { "user_agent.original": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "count()": 120, }, ], }); }, ), ); const result = await searchEvents.handler( { organizationSlug: "test-org", naturalLanguageQuery: "which user agents have the most tool calls yesterday", limit: 10, includeExplanation: false, }, { constraints: { organizationSlug: null, }, accessToken: "test-token", userId: "1", }, ); expect(mockGenerateText).toHaveBeenCalled(); expect(result).toContain("Mozilla/5.0"); expect(result).toContain("150"); expect(result).toContain("120"); // Should NOT contain user.id references expect(result).not.toContain("user.id"); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/getsentry/sentry-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server