Skip to content

Commit cfed611

Browse files
committed
chore: Add cursor rules for AI integrations contributions
1 parent bba6a80 commit cfed611

1 file changed

Lines changed: 337 additions & 0 deletions

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
---
2+
description: Guidelines for contributing a new Sentry JavaScript SDK AI integration.
3+
alwaysApply: true
4+
---
5+
6+
# Adding a New AI Integration
7+
8+
Use these guidelines when contributing a new Sentry JavaScript SDK AI integration.
9+
10+
## Quick Decision Tree
11+
12+
**CRITICAL**
13+
14+
```
15+
Does the AI SDK have native OpenTelemetry support?
16+
├─ YES → Does it emit OTel spans automatically?
17+
│ ├─ YES (like Vercel AI) → Pattern 1: OTEL Span Processors
18+
│ └─ NO → Pattern 2: OTEL Instrumentation (wrap client)
19+
└─ NO → Does the SDK provide hooks/callbacks?
20+
├─ YES (like LangChain) → Pattern 3: Callback/Hook Based
21+
└─ NO → Pattern 4: Client Wrapping
22+
23+
Multi-runtime considerations:
24+
- Node.js: Use OpenTelemetry instrumentation
25+
- Edge (Cloudflare/Vercel): No OTel, processors only or manual wrapping
26+
```
27+
28+
---
29+
30+
## Span Hierarchy
31+
32+
**Two span types:**
33+
- `gen_ai.invoke_agent` - Parent/pipeline spans (chains, agents, orchestration)
34+
- `gen_ai.chat`, `gen_ai.generate_text`, etc. - Child spans (actual LLM calls)
35+
36+
**Hierarchy example:**
37+
```
38+
gen_ai.invoke_agent (ai.generateText)
39+
└── gen_ai.generate_text (ai.generateText.doGenerate)
40+
```
41+
42+
**References:**
43+
- Vercel AI: `packages/core/src/tracing/vercel-ai/constants.ts:8-23`
44+
- LangChain: `packages/core/src/tracing/langchain/index.ts:199-207`
45+
46+
---
47+
48+
## Streaming vs Non-Streaming
49+
50+
**Non-streaming:** Use `startSpan()`, set attributes immediately from response
51+
52+
**Streaming:** Use `startSpanManual()` with this pattern:
53+
```typescript
54+
interface StreamingState {
55+
responseTexts: string[]; // Accumulate fragments
56+
promptTokens: number | undefined;
57+
completionTokens: number | undefined;
58+
// ...
59+
}
60+
61+
async function* instrumentStream(stream, span, recordOutputs) {
62+
const state: StreamingState = { responseTexts: [], ... };
63+
try {
64+
for await (const event of stream) {
65+
processEvent(event, state, recordOutputs); // Accumulate data
66+
yield event; // Pass through
67+
}
68+
} finally {
69+
setTokenUsageAttributes(span, state.promptTokens, state.completionTokens);
70+
span.setAttributes({ [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true });
71+
span.end(); // MUST call manually
72+
}
73+
}
74+
```
75+
76+
**Key rules:**
77+
- Accumulate with arrays/strings, don't overwrite
78+
- Set `GEN_AI_RESPONSE_STREAMING_ATTRIBUTE: true`
79+
- Call `span.end()` in finally block
80+
81+
**References:**
82+
- OpenAI: `packages/core/src/tracing/openai/streaming.ts`
83+
- Anthropic: `packages/core/src/tracing/anthropic-ai/streaming.ts`
84+
- Detection: `packages/core/src/tracing/openai/index.ts:183-221`
85+
86+
---
87+
88+
## Token Accumulation
89+
90+
**Child spans (LLM calls):** Set tokens directly from API response
91+
```typescript
92+
setTokenUsageAttributes(span, inputTokens, outputTokens, totalTokens);
93+
```
94+
95+
**Parent spans (invoke_agent):** Accumulate from children using event processor
96+
```typescript
97+
// First pass: accumulate from children
98+
for (const span of event.spans) {
99+
if (span.parent_span_id && isGenAiOperationSpan(span)) {
100+
accumulateTokensForParent(span, tokenAccumulator);
101+
}
102+
}
103+
104+
// Second pass: apply to invoke_agent parents
105+
for (const span of event.spans) {
106+
if (span.op === 'gen_ai.invoke_agent') {
107+
applyAccumulatedTokens(span, tokenAccumulator);
108+
}
109+
}
110+
```
111+
112+
**Reference:** `packages/core/src/tracing/vercel-ai/index.ts:110-140`
113+
114+
---
115+
116+
## Shared Utilities
117+
118+
Location: `packages/core/src/tracing/ai/`
119+
120+
### `gen-ai-attributes.ts`
121+
122+
OpenTelemetry Semantic Convention attribute names. **Always use these constants!**
123+
- `GEN_AI_SYSTEM_ATTRIBUTE` - 'openai', 'anthropic', etc.
124+
- `GEN_AI_REQUEST_MODEL_ATTRIBUTE` - Model from request
125+
- `GEN_AI_RESPONSE_MODEL_ATTRIBUTE` - Model from response
126+
- `GEN_AI_INPUT_MESSAGES_ATTRIBUTE` - Input (requires recordInputs)
127+
- `GEN_AI_RESPONSE_TEXT_ATTRIBUTE` - Output (requires recordOutputs)
128+
- `GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE` - Token counts
129+
- `GEN_AI_OPERATION_NAME_ATTRIBUTE` - 'chat', 'embeddings', etc.
130+
131+
### `utils.ts`
132+
133+
- `setTokenUsageAttributes()` - Set token usage on span
134+
- `getTruncatedJsonString()` - Truncate for attributes
135+
- `truncateGenAiMessages()` - Truncate message arrays
136+
- `buildMethodPath()` - Build method path from traversal
137+
138+
---
139+
140+
## Pattern 1: OTEL Span Processors
141+
142+
**Use when:** SDK emits OTel spans automatically (Vercel AI)
143+
144+
### Key Steps
145+
146+
1. **Core:** Create `add{Provider}Processors()` in `packages/core/src/tracing/{provider}/index.ts`
147+
- Registers `spanStart` listener + event processor
148+
- Post-processes spans to match semantic conventions
149+
150+
2. **Node.js:** Add performance optimization in `packages/node/src/integrations/tracing/{provider}/index.ts`
151+
- Use `callWhenPatched()` to defer processor registration
152+
- Only register when package is actually imported (see vercelai:36)
153+
154+
3. **Edge:** Direct registration in `packages/cloudflare/src/integrations/tracing/{provider}.ts`
155+
- No OTel patching available
156+
- Just call `add{Provider}Processors()` immediately
157+
158+
**Reference:** `packages/node/src/integrations/tracing/vercelai/`
159+
160+
---
161+
162+
## Pattern 2: OTEL Instrumentation (Client Wrapping)
163+
164+
**Use when:** SDK has NO native OTel support (OpenAI, Anthropic, Google GenAI)
165+
166+
### Key Steps
167+
168+
1. **Core:** Create `instrument{Provider}Client()` in `packages/core/src/tracing/{provider}/index.ts`
169+
- Use Proxy to wrap client methods recursively
170+
- Create spans manually with `startSpan()` or `startSpanManual()`
171+
172+
2. **Node.js Instrumentation:** Patch module exports in `instrumentation.ts`
173+
- Wrap client constructor
174+
- Check `_INTERNAL_shouldSkipAiProviderWrapping()` (for LangChain)
175+
- See openai/instrumentation.ts:70-86
176+
177+
3. **Node.js Integration:** Export instrumentation function
178+
- Use `generateInstrumentOnce()` helper
179+
- See openai/index.ts:6-9
180+
181+
**Reference:** `packages/node/src/integrations/tracing/openai/`
182+
183+
---
184+
185+
## Pattern 3: Callback/Hook Based
186+
187+
**Use when:** SDK provides lifecycle hooks (LangChain, LangGraph)
188+
189+
### Key Steps
190+
191+
1. **Core:** Create `create{Provider}CallbackHandler()` in `packages/core/src/tracing/{provider}/index.ts`
192+
- Implement SDK's callback interface
193+
- Create spans in callback methods
194+
195+
2. **Node.js Instrumentation:** Auto-inject callbacks
196+
- Patch runnable methods to add handler automatically
197+
- **Important:** Disable underlying AI provider wrapping (langchain/instrumentation.ts:103-105)
198+
199+
**Reference:** `packages/node/src/integrations/tracing/langchain/`
200+
201+
---
202+
203+
## Auto-Instrumentation (Out-of-the-Box Support)
204+
205+
**RULE:** AI SDKs should be auto-enabled in Node.js runtime if possible.
206+
207+
✅ **Auto-enable if:**
208+
- SDK works in Node.js runtime
209+
- OTel only patches when package imported (zero cost if unused)
210+
211+
❌ **Don't auto-enable if:**
212+
- SDK is niche/experimental
213+
- Integration has significant limitations
214+
215+
### Steps to Auto-Enable
216+
217+
**1. Add to auto performance integrations**
218+
219+
Location: `packages/node/src/integrations/tracing/index.ts`
220+
221+
```typescript
222+
export function getAutoPerformanceIntegrations(): Integration[] {
223+
return [
224+
// AI providers - IMPORTANT: LangChain MUST come first!
225+
langChainIntegration(), // Disables underlying providers
226+
langGraphIntegration(),
227+
vercelAIIntegration(),
228+
openAIIntegration(),
229+
anthropicAIIntegration(),
230+
googleGenAIIntegration(),
231+
{provider}Integration(), // <-- Add here
232+
];
233+
}
234+
```
235+
236+
**2. Add to preload instrumentation**
237+
238+
```typescript
239+
export function getOpenTelemetryInstrumentationToPreload() {
240+
return [
241+
instrumentOpenAi,
242+
instrumentAnthropicAi,
243+
instrument{Provider}, // <-- Add here
244+
];
245+
}
246+
```
247+
248+
**3. Export from package index**
249+
250+
```typescript
251+
// packages/node/src/index.ts
252+
export { {provider}Integration } from './integrations/tracing/{provider}';
253+
export type { {Provider}Options } from './integrations/tracing/{provider}';
254+
255+
// If browser-compatible: packages/browser/src/index.ts
256+
export { {provider}Integration } from './integrations/tracing/{provider}';
257+
```
258+
259+
**4. Add E2E test** in `packages/node-integration-tests/suites/{provider}/`
260+
- Verify spans created automatically (no manual setup)
261+
- Test `recordInputs` and `recordOutputs` options
262+
- Test integration can be disabled
263+
264+
---
265+
266+
## Directory Structure
267+
268+
```
269+
packages/
270+
├── core/src/tracing/
271+
│ ├── ai/ # Shared utilities
272+
│ │ ├── gen-ai-attributes.ts
273+
│ │ ├── utils.ts
274+
│ │ └── messageTruncation.ts
275+
│ └── {provider}/ # Provider-specific
276+
│ ├── index.ts # Main logic
277+
│ ├── types.ts
278+
│ ├── constants.ts
279+
│ └── streaming.ts
280+
281+
├── node/src/integrations/tracing/{provider}/
282+
│ ├── index.ts # Integration definition
283+
│ └── instrumentation.ts # OTel instrumentation
284+
285+
├── cloudflare/src/integrations/tracing/
286+
│ └── {provider}.ts # Single file
287+
288+
└── vercel-edge/src/integrations/tracing/
289+
└── {provider}.ts # Single file
290+
```
291+
292+
---
293+
294+
## Key Best Practices
295+
296+
1. **Respect `sendDefaultPii`** for recordInputs/recordOutputs
297+
2. **Use semantic attributes** from `gen-ai-attributes.ts` (never hardcode)
298+
3. **Set Sentry origin**: `SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'auto.ai.{provider}'`
299+
4. **Truncate large data**: Use helper functions from `utils.ts`
300+
5. **Correct span operations**: `gen_ai.invoke_agent` for parent, `gen_ai.chat` for children
301+
6. **Streaming**: Use `startSpanManual()`, accumulate state, call `span.end()`
302+
7. **Token accumulation**: Direct on child spans, accumulate on parent from children
303+
8. **Performance**: Use `callWhenPatched()` for Pattern 1
304+
9. **LangChain**: Check `_INTERNAL_shouldSkipAiProviderWrapping()` in Pattern 2
305+
306+
---
307+
308+
## Reference Implementations
309+
310+
- **Pattern 1 (Span Processors):** `packages/node/src/integrations/tracing/vercelai/`
311+
- **Pattern 2 (Client Wrapping):** `packages/node/src/integrations/tracing/openai/`
312+
- **Pattern 3 (Callback/Hooks):** `packages/node/src/integrations/tracing/langchain/`
313+
314+
---
315+
316+
## Auto-Instrumentation Checklist
317+
318+
- [ ] Added to `getAutoPerformanceIntegrations()` in correct order
319+
- [ ] Added to `getOpenTelemetryInstrumentationToPreload()`
320+
- [ ] Exported from `packages/node/src/index.ts`
321+
- [ ] **If browser-compatible:** Exported from `packages/browser/src/index.ts`
322+
- [ ] Added E2E test in `packages/node-integration-tests/suites/{provider}/`
323+
- [ ] E2E test verifies auto-instrumentation
324+
- [ ] JSDoc says "enabled by default" or "not enabled by default"
325+
- [ ] Documented how to disable (if auto-enabled)
326+
- [ ] Documented limitations clearly
327+
- [ ] Verified OTel only patches when package imported
328+
329+
---
330+
331+
## Questions?
332+
333+
1. Look at reference implementations above
334+
2. Check shared utilities in `packages/core/src/tracing/ai/`
335+
3. Review OpenTelemetry Semantic Conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/
336+
337+
**When in doubt, follow the pattern of the most similar existing integration!**

0 commit comments

Comments
 (0)