From 001588f96f95689fc0b55c8363872573a6a7c421 Mon Sep 17 00:00:00 2001 From: Cody De Arkland Date: Wed, 11 Feb 2026 13:27:22 -0800 Subject: [PATCH 1/3] Add competitive research agent for Sentry vs competitor analysis Introduces a new agent that provides competitive intelligence analysis, comparing Sentry against other error monitoring and observability products (Datadog, New Relic, Bugsnag, Rollbar, etc.) with structured feature comparisons, pricing analysis, and developer experience insights. Co-Authored-By: Claude Opus 4.6 --- .agents/skills/competitive-research/SKILL.md | 101 +++++ .claude/skills/competitive-research | 1 + src/app/api/competitive-research/route.ts | 161 +++++++ src/components/desktop/Desktop.tsx | 30 +- .../desktop/apps/CompetitiveResearch.tsx | 414 ++++++++++++++++++ 5 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/competitive-research/SKILL.md create mode 120000 .claude/skills/competitive-research create mode 100644 src/app/api/competitive-research/route.ts create mode 100644 src/components/desktop/apps/CompetitiveResearch.tsx diff --git a/.agents/skills/competitive-research/SKILL.md b/.agents/skills/competitive-research/SKILL.md new file mode 100644 index 0000000..dc55511 --- /dev/null +++ b/.agents/skills/competitive-research/SKILL.md @@ -0,0 +1,101 @@ +--- +name: competitive-research +description: Research and compare Sentry against competing error monitoring and observability products. Use when asked to compare Sentry with competitors like Datadog, New Relic, Bugsnag, Rollbar, Raygun, Honeybadger, or other APM/error tracking tools. Provides competitive analysis, feature comparisons, pricing insights, and differentiation points. +--- + +# Competitive Research Agent + +You are a competitive intelligence analyst specializing in the error monitoring, application performance monitoring (APM), and observability space. Your job is to research and compare Sentry against its competitors to help the team understand market positioning and differentiation. + +## Core Competitors to Track + +### Error Monitoring / Crash Reporting +- **Bugsnag** - Error monitoring for web and mobile +- **Rollbar** - Real-time error tracking and debugging +- **Raygun** - Crash reporting and real user monitoring +- **Honeybadger** - Exception monitoring for developers +- **Airbrake** - Error monitoring and performance insights + +### APM / Full-Stack Observability +- **Datadog** - Cloud monitoring and observability platform +- **New Relic** - Full-stack observability platform +- **Dynatrace** - Software intelligence platform +- **Splunk/SignalFx** - Observability and log management +- **Elastic APM** - Application performance monitoring +- **Grafana** - Open-source observability stack (with Tempo, Loki, Mimir) + +### Emerging / Niche +- **Highlight.io** - Open-source session replay + error monitoring +- **PostHog** - Product analytics with error tracking +- **OpenTelemetry** - Open standard (not a product, but relevant ecosystem) + +## Research Methodology + +### Phase 1: Understand the Query +- Identify which competitor(s) the user wants to compare +- Determine the comparison dimensions (features, pricing, developer experience, integrations, etc.) +- Clarify the use case context (startup vs enterprise, specific language/framework, etc.) + +### Phase 2: Gather Current Information +- Search the web for the latest product updates, pricing pages, and feature announcements +- Look for recent reviews, comparisons, and analyst reports +- Check for community sentiment on developer forums and social media +- Find migration guides or switching stories + +### Phase 3: Structured Analysis +For each comparison, provide: + +#### Feature Comparison +| Capability | Sentry | Competitor | +|-----------|--------|------------| +| Error Tracking | ... | ... | +| Performance Monitoring | ... | ... | +| Session Replay | ... | ... | +| Profiling | ... | ... | +| Cron Monitoring | ... | ... | +| Release Health | ... | ... | +| Code Coverage | ... | ... | +| AI Features (Seer) | ... | ... | + +#### Key Differentiators +- What Sentry does better +- What the competitor does better +- Where they overlap + +#### Developer Experience +- SDK quality and language coverage +- Documentation quality +- Open-source commitment +- Community and ecosystem + +#### Pricing & Business Model +- Free tier comparison +- Pricing model differences (event-based, host-based, user-based) +- Enterprise considerations + +### Phase 4: Deliver Insights +- Lead with the most impactful findings +- Provide specific, factual comparisons (not vague claims) +- Include source links where possible +- Highlight recent changes or announcements that shift the competitive landscape +- Be honest about areas where competitors may have advantages + +## Guidelines + +- **Be factual and balanced** - Acknowledge competitor strengths honestly. Credibility comes from objectivity. +- **Stay current** - Always search for the latest information. Product landscapes change rapidly. +- **Focus on developer perspective** - Sentry's audience is developers. Compare through that lens. +- **Cite sources** - Link to official docs, pricing pages, blog posts, and reviews when possible. +- **Use tables for comparisons** - Structured data is easier to digest than prose. +- **Note when information may be outdated** - If you can't verify recency, say so. +- **Consider the full stack** - Compare not just core features but also integrations, SDKs, documentation, and community. + +## Output Format + +Always structure responses with: +1. **Executive Summary** - 2-3 sentence overview of the comparison +2. **Detailed Comparison** - Feature tables and analysis +3. **Sentry Advantages** - Where Sentry wins +4. **Competitor Advantages** - Where the competitor wins +5. **Recommendation** - Contextual guidance based on the user's needs +6. **Sources** - Links to references used diff --git a/.claude/skills/competitive-research b/.claude/skills/competitive-research new file mode 120000 index 0000000..af142b4 --- /dev/null +++ b/.claude/skills/competitive-research @@ -0,0 +1 @@ +../../../.agents/skills/competitive-research \ No newline at end of file diff --git a/src/app/api/competitive-research/route.ts b/src/app/api/competitive-research/route.ts new file mode 100644 index 0000000..5823bc9 --- /dev/null +++ b/src/app/api/competitive-research/route.ts @@ -0,0 +1,161 @@ +import { query } from '@anthropic-ai/claude-agent-sdk' +import * as Sentry from '@sentry/nextjs' + +const SYSTEM_PROMPT = `You are a competitive intelligence analyst specializing in the error monitoring, application performance monitoring (APM), and observability space. Your job is to research and compare Sentry against its competitors. + +Core competitors you should be familiar with: +- Error Monitoring: Bugsnag, Rollbar, Raygun, Honeybadger, Airbrake +- APM/Observability: Datadog, New Relic, Dynatrace, Splunk/SignalFx, Elastic APM, Grafana +- Emerging: Highlight.io, PostHog, OpenTelemetry ecosystem + +Your role is to: +- Compare Sentry against specific competitors when asked +- Research the latest features, pricing, and positioning of competing products +- Provide balanced, factual analysis - acknowledge competitor strengths honestly +- Use tables for feature comparisons to make data easy to digest +- Always search the web for current information - product landscapes change rapidly +- Focus on the developer experience perspective since Sentry's audience is developers + +Guidelines: +- Structure responses with: Executive Summary, Detailed Comparison, Sentry Advantages, Competitor Advantages, and Sources +- Be specific and factual - cite pricing pages, documentation, and recent announcements +- Note when information may be outdated or unverifiable +- Consider the full stack: features, SDKs, docs, community, pricing, and integrations +- Use markdown tables for side-by-side comparisons +- Include links to sources when available` + +interface MessageInput { + role: 'user' | 'assistant' + content: string +} + +export async function POST(request: Request) { + try { + const { messages } = await request.json() as { messages: MessageInput[] } + + if (!messages || !Array.isArray(messages)) { + Sentry.logger.warn('Competitive research request received with invalid messages payload') + Sentry.metrics.increment('competitive_research.requests', 1, { tags: { status: 'invalid' } }) + return new Response( + JSON.stringify({ error: 'Messages array is required' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ) + } + + const lastUserMessage = messages.filter(m => m.role === 'user').pop() + if (!lastUserMessage) { + Sentry.logger.warn('Competitive research request received with no user message') + Sentry.metrics.increment('competitive_research.requests', 1, { tags: { status: 'invalid' } }) + return new Response( + JSON.stringify({ error: 'No user message found' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ) + } + + Sentry.logger.info('Competitive research request received with %d messages', [messages.length]) + Sentry.metrics.increment('competitive_research.requests', 1, { tags: { status: 'started' } }) + Sentry.metrics.distribution('competitive_research.messages_per_request', messages.length) + + const conversationContext = messages + .slice(0, -1) + .map((m: MessageInput) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`) + .join('\n\n') + + const fullPrompt = conversationContext + ? `${SYSTEM_PROMPT}\n\nPrevious conversation:\n${conversationContext}\n\nUser: ${lastUserMessage.content}` + : `${SYSTEM_PROMPT}\n\nUser: ${lastUserMessage.content}` + + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + try { + for await (const message of query({ + prompt: fullPrompt, + options: { + maxTurns: 10, + tools: { type: 'preset', preset: 'claude_code' }, + permissionMode: 'bypassPermissions', + allowDangerouslySkipPermissions: true, + includePartialMessages: true, + cwd: process.cwd(), + } + })) { + if (message.type === 'stream_event' && 'event' in message) { + const event = message.event + if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') { + controller.enqueue(encoder.encode( + `data: ${JSON.stringify({ type: 'text_delta', text: event.delta.text })}\n\n` + )) + } + } + + if (message.type === 'assistant' && 'message' in message) { + const content = message.message?.content + if (Array.isArray(content)) { + for (const block of content) { + if (block.type === 'tool_use') { + Sentry.logger.info('Competitive research tool invoked: %s', [block.name]) + Sentry.metrics.increment('competitive_research.tool_invocations', 1, { tags: { tool: block.name } }) + controller.enqueue(encoder.encode( + `data: ${JSON.stringify({ type: 'tool_start', tool: block.name })}\n\n` + )) + } + } + } + } + + if (message.type === 'tool_progress') { + controller.enqueue(encoder.encode( + `data: ${JSON.stringify({ type: 'tool_progress', tool: message.tool_name, elapsed: message.elapsed_time_seconds })}\n\n` + )) + } + + if (message.type === 'result' && message.subtype === 'success') { + Sentry.logger.info('Competitive research stream completed successfully') + Sentry.metrics.increment('competitive_research.requests', 1, { tags: { status: 'success' } }) + controller.enqueue(encoder.encode( + `data: ${JSON.stringify({ type: 'done' })}\n\n` + )) + } + + if (message.type === 'result' && message.subtype !== 'success') { + Sentry.logger.error('Competitive research query did not complete successfully, subtype: %s', [message.subtype]) + Sentry.metrics.increment('competitive_research.requests', 1, { tags: { status: 'query_failure' } }) + controller.enqueue(encoder.encode( + `data: ${JSON.stringify({ type: 'error', message: 'Query did not complete successfully' })}\n\n` + )) + } + } + + controller.enqueue(encoder.encode('data: [DONE]\n\n')) + controller.close() + } catch (error) { + Sentry.logger.error('Competitive research stream error: %s', [error instanceof Error ? error.message : String(error)]) + Sentry.metrics.increment('competitive_research.errors', 1, { tags: { phase: 'stream' } }) + Sentry.captureException(error) + controller.enqueue(encoder.encode( + `data: ${JSON.stringify({ type: 'error', message: 'Stream error occurred' })}\n\n` + )) + controller.close() + } + } + }) + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }) + } catch (error) { + Sentry.logger.error('Competitive research API error: %s', [error instanceof Error ? error.message : String(error)]) + Sentry.metrics.increment('competitive_research.errors', 1, { tags: { phase: 'request' } }) + Sentry.captureException(error) + + return new Response( + JSON.stringify({ error: 'Failed to process competitive research request. Check server logs for details.' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ) + } +} diff --git a/src/components/desktop/Desktop.tsx b/src/components/desktop/Desktop.tsx index 3abda94..7a4ecd3 100644 --- a/src/components/desktop/Desktop.tsx +++ b/src/components/desktop/Desktop.tsx @@ -7,6 +7,7 @@ import { DesktopIcon } from './DesktopIcon' import { Notepad } from './apps/Notepad' import { FolderView, FolderItem } from './apps/FolderView' import { Chat } from './apps/Chat' +import { CompetitiveResearch } from './apps/CompetitiveResearch' import { useState } from 'react' import * as Sentry from '@sentry/nextjs' @@ -97,10 +98,37 @@ function DesktopContent() { }) } + const openCompetitiveResearch = () => { + Sentry.logger.info('App launched: %s', ['Competitive Research']) + Sentry.metrics.increment('app.launched', 1, { tags: { app: 'competitive-research' } }) + openWindow({ + id: 'competitive-research', + title: 'Competitive Research', + icon: '⚔️', + x: 250, + y: 60, + width: 600, + height: 600, + minWidth: 400, + minHeight: 400, + isMinimized: false, + isMaximized: false, + content: + }) + } + const openAgentsFolder = () => { Sentry.logger.info('App launched: %s', ['Agents Folder']) Sentry.metrics.increment('app.launched', 1, { tags: { app: 'agents-folder' } }) - const agentsFolderItems: FolderItem[] = [] + const agentsFolderItems: FolderItem[] = [ + { + id: 'competitive-research', + name: 'Competitive Research', + type: 'app', + icon: 'chat', + onOpen: openCompetitiveResearch, + }, + ] openWindow({ id: 'agents-folder', diff --git a/src/components/desktop/apps/CompetitiveResearch.tsx b/src/components/desktop/apps/CompetitiveResearch.tsx new file mode 100644 index 0000000..6e77d25 --- /dev/null +++ b/src/components/desktop/apps/CompetitiveResearch.tsx @@ -0,0 +1,414 @@ +'use client' + +import { useState, useRef, useEffect } from 'react' +import { Send, Bot, User, Loader2, Wrench, Search, Globe, FileText, Terminal, Swords } from 'lucide-react' +import * as Sentry from '@sentry/nextjs' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism' + +interface Message { + id: string + role: 'user' | 'assistant' + content: string + timestamp: Date +} + +interface ToolStatus { + name: string + status: 'running' | 'complete' + elapsed?: number +} + +const toolDisplayInfo: Record = { + 'WebSearch': { name: 'Web Search', icon: 'search' }, + 'WebFetch': { name: 'Fetching URL', icon: 'globe' }, + 'Read': { name: 'Reading File', icon: 'file' }, + 'Write': { name: 'Writing File', icon: 'file' }, + 'Edit': { name: 'Editing File', icon: 'file' }, + 'Glob': { name: 'Finding Files', icon: 'file' }, + 'Grep': { name: 'Searching Content', icon: 'search' }, + 'Bash': { name: 'Running Command', icon: 'terminal' }, + 'Task': { name: 'Running Task', icon: 'wrench' }, +} + +const ToolIcon = ({ type }: { type: 'search' | 'globe' | 'file' | 'terminal' | 'wrench' }) => { + const iconClass = "w-3 h-3" + switch (type) { + case 'search': return + case 'globe': return + case 'file': return + case 'terminal': return + default: return + } +} + +const QUICK_PROMPTS = [ + 'Compare Sentry vs Datadog', + 'Compare Sentry vs New Relic', + 'Compare Sentry vs Bugsnag', + 'Sentry pricing vs competitors', +] + +export function CompetitiveResearch() { + const [messages, setMessages] = useState([ + { + id: 'welcome', + role: 'assistant', + content: `Welcome to the **Competitive Research Agent**. I can help you compare Sentry against competitors in the error monitoring and observability space. + +Ask me about: +- **Feature comparisons** - "How does Sentry compare to Datadog for error tracking?" +- **Pricing analysis** - "Compare Sentry and New Relic pricing models" +- **Developer experience** - "Which has better Python SDK support, Sentry or Rollbar?" +- **Market positioning** - "What are Sentry's key differentiators vs Bugsnag?" + +I'll search for the latest information and provide balanced, factual analysis.`, + timestamp: new Date() + } + ]) + const [input, setInput] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [currentTool, setCurrentTool] = useState(null) + const messagesEndRef = useRef(null) + const inputRef = useRef(null) + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + + useEffect(() => { + scrollToBottom() + }, [messages, currentTool]) + + const sendMessage = async (messageText: string) => { + if (!messageText.trim() || isLoading) return + + const userMessage: Message = { + id: crypto.randomUUID(), + role: 'user', + content: messageText.trim(), + timestamp: new Date() + } + + setMessages(prev => [...prev, userMessage]) + setInput('') + setIsLoading(true) + setCurrentTool(null) + + Sentry.logger.info('User sent competitive research query, conversation length: %d', [messages.length + 1]) + Sentry.metrics.increment('competitive_research.client.message_sent', 1) + + try { + const response = await fetch('/api/competitive-research', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + messages: [...messages, userMessage].map(m => ({ + role: m.role, + content: m.content + })) + }) + }) + + if (!response.ok) { + throw new Error('Failed to get response') + } + + const reader = response.body?.getReader() + if (!reader) { + throw new Error('No response body') + } + + const decoder = new TextDecoder() + let streamingContent = '' + const streamingMessageId = crypto.randomUUID() + + setMessages(prev => [...prev, { + id: streamingMessageId, + role: 'assistant', + content: '', + timestamp: new Date() + }]) + + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value) + const lines = chunk.split('\n') + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6) + if (data === '[DONE]') continue + + try { + const parsed = JSON.parse(data) + + if (parsed.type === 'text_delta') { + streamingContent += parsed.text + setCurrentTool(null) + setMessages(prev => prev.map(msg => + msg.id === streamingMessageId + ? { ...msg, content: streamingContent } + : msg + )) + } else if (parsed.type === 'tool_start') { + Sentry.logger.info('Tool execution started: %s', [parsed.tool]) + Sentry.metrics.increment('competitive_research.client.tool_execution', 1, { tags: { tool: parsed.tool } }) + setCurrentTool({ + name: parsed.tool, + status: 'running' + }) + } else if (parsed.type === 'tool_progress') { + setCurrentTool(prev => prev ? { + ...prev, + elapsed: parsed.elapsed + } : null) + } else if (parsed.type === 'done') { + Sentry.logger.info('Competitive research response stream completed') + Sentry.metrics.increment('competitive_research.client.response_received', 1) + setCurrentTool(null) + } else if (parsed.type === 'error') { + Sentry.logger.error('Competitive research stream returned error: %s', [parsed.message]) + streamingContent = 'Sorry, I encountered an error processing your request.' + setMessages(prev => prev.map(msg => + msg.id === streamingMessageId + ? { ...msg, content: streamingContent } + : msg + )) + setCurrentTool(null) + } + } catch { + // Ignore parse errors for incomplete chunks + } + } + } + } + + if (!streamingContent) { + setMessages(prev => prev.filter(msg => msg.id !== streamingMessageId)) + } + } catch (error) { + Sentry.logger.error('Competitive research fetch error: %s', [error instanceof Error ? error.message : String(error)]) + Sentry.metrics.increment('competitive_research.client.errors', 1) + Sentry.captureException(error) + const errorMessage: Message = { + id: crypto.randomUUID(), + role: 'assistant', + content: 'Sorry, I encountered an error. Please check your Claude credentials are configured correctly.', + timestamp: new Date() + } + setMessages(prev => [...prev, errorMessage]) + } finally { + setIsLoading(false) + setCurrentTool(null) + } + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + await sendMessage(input) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSubmit(e) + } + } + + return ( +
+ {/* Header */} +
+ + Competitive Research + Powered by Claude +
+ + {/* Messages */} +
+ {messages.map((message) => ( +
+
+ {message.role === 'user' ? ( + + ) : ( + + )} +
+
+
+ + {children} + + ) : ( + + {String(children).replace(/\n$/, '')} + + ) + }, + a({ href, children }) { + return ( + + {children} + + ) + }, + ul({ children }) { + return
    {children}
+ }, + ol({ children }) { + return
    {children}
+ }, + li({ children }) { + return
  • {children}
  • + }, + p({ children }) { + return

    {children}

    + }, + h1({ children }) { + return

    {children}

    + }, + h2({ children }) { + return

    {children}

    + }, + h3({ children }) { + return

    {children}

    + }, + blockquote({ children }) { + return
    {children}
    + }, + table({ children }) { + return {children}
    + }, + th({ children }) { + return {children} + }, + td({ children }) { + return {children} + }, + hr() { + return
    + }, + }} + > + {message.content} +
    +
    + + {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + +
    +
    + ))} + + {isLoading && ( +
    +
    + +
    +
    + {currentTool ? ( +
    +
    + + + {toolDisplayInfo[currentTool.name]?.name || currentTool.name} + +
    + + {currentTool.elapsed !== undefined && ( + + {currentTool.elapsed.toFixed(1)}s + + )} +
    + ) : ( +
    + + Researching... +
    + )} +
    +
    + )} + +
    +
    + + {/* Quick prompts - only show when no user messages yet */} + {messages.length === 1 && !isLoading && ( +
    + {QUICK_PROMPTS.map((prompt) => ( + + ))} +
    + )} + + {/* Input */} +
    +
    +