Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .agents/skills/competitive-research/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .claude/skills/competitive-research
18 changes: 9 additions & 9 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function POST(request: Request) {

if (!messages || !Array.isArray(messages)) {
Sentry.logger.warn('Chat request received with invalid messages payload')
Sentry.metrics.increment('chat.requests', 1, { tags: { status: 'invalid' } })
Sentry.metrics?.increment?.('chat.requests', 1, { tags: { status: 'invalid' } })
return new Response(
JSON.stringify({ error: 'Messages array is required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
Expand All @@ -39,16 +39,16 @@ export async function POST(request: Request) {
const lastUserMessage = messages.filter(m => m.role === 'user').pop()
if (!lastUserMessage) {
Sentry.logger.warn('Chat request received with no user message')
Sentry.metrics.increment('chat.requests', 1, { tags: { status: 'invalid' } })
Sentry.metrics?.increment?.('chat.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('Chat request received with %d messages', [messages.length])
Sentry.metrics.increment('chat.requests', 1, { tags: { status: 'started' } })
Sentry.metrics.distribution('chat.messages_per_request', messages.length)
Sentry.metrics?.increment?.('chat.requests', 1, { tags: { status: 'started' } })
Sentry.metrics?.distribution?.('chat.messages_per_request', messages.length)

// Build conversation context
const conversationContext = messages
Expand Down Expand Up @@ -99,7 +99,7 @@ export async function POST(request: Request) {
for (const block of content) {
if (block.type === 'tool_use') {
Sentry.logger.info('Agent tool invoked: %s', [block.name])
Sentry.metrics.increment('chat.tool_invocations', 1, { tags: { tool: block.name } })
Sentry.metrics?.increment?.('chat.tool_invocations', 1, { tags: { tool: block.name } })
controller.enqueue(encoder.encode(
`data: ${JSON.stringify({ type: 'tool_start', tool: block.name })}\n\n`
))
Expand All @@ -118,7 +118,7 @@ export async function POST(request: Request) {
// Signal completion
if (message.type === 'result' && message.subtype === 'success') {
Sentry.logger.info('Chat stream completed successfully')
Sentry.metrics.increment('chat.requests', 1, { tags: { status: 'success' } })
Sentry.metrics?.increment?.('chat.requests', 1, { tags: { status: 'success' } })
controller.enqueue(encoder.encode(
`data: ${JSON.stringify({ type: 'done' })}\n\n`
))
Expand All @@ -127,7 +127,7 @@ export async function POST(request: Request) {
// Handle errors
if (message.type === 'result' && message.subtype !== 'success') {
Sentry.logger.error('Chat query did not complete successfully, subtype: %s', [message.subtype])
Sentry.metrics.increment('chat.requests', 1, { tags: { status: 'query_failure' } })
Sentry.metrics?.increment?.('chat.requests', 1, { tags: { status: 'query_failure' } })
controller.enqueue(encoder.encode(
`data: ${JSON.stringify({ type: 'error', message: 'Query did not complete successfully' })}\n\n`
))
Expand All @@ -138,7 +138,7 @@ export async function POST(request: Request) {
controller.close()
} catch (error) {
Sentry.logger.error('Chat stream error: %s', [error instanceof Error ? error.message : String(error)])
Sentry.metrics.increment('chat.errors', 1, { tags: { phase: 'stream' } })
Sentry.metrics?.increment?.('chat.errors', 1, { tags: { phase: 'stream' } })
Sentry.captureException(error)
controller.enqueue(encoder.encode(
`data: ${JSON.stringify({ type: 'error', message: 'Stream error occurred' })}\n\n`
Expand All @@ -157,7 +157,7 @@ export async function POST(request: Request) {
})
} catch (error) {
Sentry.logger.error('Chat API error: %s', [error instanceof Error ? error.message : String(error)])
Sentry.metrics.increment('chat.errors', 1, { tags: { phase: 'request' } })
Sentry.metrics?.increment?.('chat.errors', 1, { tags: { phase: 'request' } })
Sentry.captureException(error)

return new Response(
Expand Down
161 changes: 161 additions & 0 deletions src/app/api/competitive-research/route.ts
Original file line number Diff line number Diff line change
@@ -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(),
}
Comment on lines +73 to +81
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The system prompt is concatenated into the prompt string instead of being passed to the dedicated systemPrompt option in the query() call, leading to wasted tokens.
Severity: MEDIUM

Suggested Fix

Refactor the query() call to separate the system prompt from the user prompt. Pass the SYSTEM_PROMPT constant to the systemPrompt property within the options object. The prompt parameter should only contain the user's message and conversation context.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/app/api/competitive-research/route.ts#L73-L81

Potential issue: The `query()` function for the Claude Agent SDK is called with the
system prompt concatenated directly into the `prompt` parameter. The SDK provides a
dedicated `systemPrompt` option for this purpose. By combining them, the system prompt
is resent with every turn in a multi-turn conversation, which wastes tokens and
increases costs. This also risks the model misinterpreting system-level directives as
part of the user's query, potentially leading to incorrect or inconsistent behavior. The
new `competitive-research` route introduces this incorrect pattern.

})) {
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' } }
)
}
}
Loading