Skip to content

Commit e03f0b8

Browse files
authored
Merge branch 'develop' into genai-msg-limit
2 parents 7df9993 + a524022 commit e03f0b8

File tree

6 files changed

+170
-5
lines changed

6 files changed

+170
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
Work in this release was contributed by @stefanvanderwolf. Thank you for your contribution!
8+
79
## 10.17.0
810

911
### Important Changes
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as Sentry from '@sentry/node';
2+
import { generateObject } from 'ai';
3+
import { MockLanguageModelV1 } from 'ai/test';
4+
import { z } from 'zod';
5+
6+
async function run() {
7+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
8+
// Test generateObject with schema
9+
await generateObject({
10+
model: new MockLanguageModelV1({
11+
defaultObjectGenerationMode: 'json',
12+
doGenerate: async () => ({
13+
rawCall: { rawPrompt: null, rawSettings: {} },
14+
finishReason: 'stop',
15+
usage: { promptTokens: 15, completionTokens: 25 },
16+
text: '{ "name": "John Doe", "age": 30 }',
17+
}),
18+
}),
19+
schema: z.object({
20+
name: z.string().describe('The name of the person'),
21+
age: z.number().describe('The age of the person'),
22+
}),
23+
schemaName: 'Person',
24+
schemaDescription: 'A person with name and age',
25+
prompt: 'Generate a person object',
26+
});
27+
});
28+
}
29+
30+
run();
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
3+
4+
describe('Vercel AI integration - generateObject', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
const EXPECTED_TRANSACTION = {
10+
transaction: 'main',
11+
spans: expect.arrayContaining([
12+
// generateObject span
13+
expect.objectContaining({
14+
data: expect.objectContaining({
15+
'vercel.ai.model.id': 'mock-model-id',
16+
'vercel.ai.model.provider': 'mock-provider',
17+
'vercel.ai.operationId': 'ai.generateObject',
18+
'vercel.ai.pipeline.name': 'generateObject',
19+
'vercel.ai.streaming': false,
20+
'vercel.ai.settings.mode': 'json',
21+
'vercel.ai.settings.output': 'object',
22+
'gen_ai.request.schema': expect.any(String),
23+
'gen_ai.response.model': 'mock-model-id',
24+
'gen_ai.usage.input_tokens': 15,
25+
'gen_ai.usage.output_tokens': 25,
26+
'gen_ai.usage.total_tokens': 40,
27+
'operation.name': 'ai.generateObject',
28+
'sentry.op': 'gen_ai.invoke_agent',
29+
'sentry.origin': 'auto.vercelai.otel',
30+
}),
31+
description: 'generateObject',
32+
op: 'gen_ai.invoke_agent',
33+
origin: 'auto.vercelai.otel',
34+
status: 'ok',
35+
}),
36+
// generateObject.doGenerate span
37+
expect.objectContaining({
38+
data: expect.objectContaining({
39+
'sentry.origin': 'auto.vercelai.otel',
40+
'sentry.op': 'gen_ai.generate_object',
41+
'operation.name': 'ai.generateObject.doGenerate',
42+
'vercel.ai.operationId': 'ai.generateObject.doGenerate',
43+
'vercel.ai.model.provider': 'mock-provider',
44+
'vercel.ai.model.id': 'mock-model-id',
45+
'vercel.ai.pipeline.name': 'generateObject.doGenerate',
46+
'vercel.ai.streaming': false,
47+
'gen_ai.system': 'mock-provider',
48+
'gen_ai.request.model': 'mock-model-id',
49+
'gen_ai.response.model': 'mock-model-id',
50+
'gen_ai.usage.input_tokens': 15,
51+
'gen_ai.usage.output_tokens': 25,
52+
'gen_ai.usage.total_tokens': 40,
53+
}),
54+
description: 'generate_object mock-model-id',
55+
op: 'gen_ai.generate_object',
56+
origin: 'auto.vercelai.otel',
57+
status: 'ok',
58+
}),
59+
]),
60+
};
61+
62+
createEsmAndCjsTests(__dirname, 'scenario-generate-object.mjs', 'instrument.mjs', (createRunner, test) => {
63+
test('captures generateObject spans with schema attributes', async () => {
64+
await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
65+
});
66+
});
67+
});

packages/core/src/utils/vercel-ai/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE,
1919
AI_RESPONSE_TEXT_ATTRIBUTE,
2020
AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
21+
AI_SCHEMA_ATTRIBUTE,
2122
AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
2223
AI_TOOL_CALL_ARGS_ATTRIBUTE,
2324
AI_TOOL_CALL_ID_ATTRIBUTE,
@@ -126,6 +127,8 @@ function processEndedVercelAiSpan(span: SpanJSON): void {
126127
renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input');
127128
renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output');
128129

130+
renameAttributeKey(attributes, AI_SCHEMA_ATTRIBUTE, 'gen_ai.request.schema');
131+
129132
addProviderMetadataToAttributes(attributes);
130133

131134
// Change attributes namespaced with `ai.X` to `vercel.ai.X`

packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,18 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
7474
return decoratorResult(target, propertyKey, descriptor);
7575
}
7676

77+
function eventNameFromEvent(event: unknown): string {
78+
if (typeof event === 'string') {
79+
return event;
80+
} else if (Array.isArray(event)) {
81+
return event.map(eventNameFromEvent).join(',');
82+
} else return String(event);
83+
}
84+
7785
const originalHandler = descriptor.value;
7886
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
7987
const handlerName = originalHandler.name || propertyKey;
80-
let eventName = typeof event === 'string' ? event : String(event);
88+
let eventName = eventNameFromEvent(event);
8189

8290
// Instrument the actual handler
8391
descriptor.value = async function (...args: unknown[]) {
@@ -93,7 +101,7 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
93101
eventName = eventData
94102
.map((data: unknown) => {
95103
if (data && typeof data === 'object' && 'event' in data && data.event) {
96-
return data.event;
104+
return eventNameFromEvent(data.event);
97105
}
98106
return '';
99107
})

packages/nestjs/test/integrations/nest.test.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,72 @@ describe('Nest', () => {
7575

7676
await descriptor.value();
7777

78-
expect(core.startSpan).toHaveBeenCalled();
78+
expect(core.startSpan).toHaveBeenCalledWith(
79+
expect.objectContaining({
80+
name: 'event test.event',
81+
}),
82+
expect.any(Function),
83+
);
7984
expect(originalHandler).toHaveBeenCalled();
8085
});
8186

82-
it('should wrap array event handlers', async () => {
87+
it('should wrap symbol event handlers', async () => {
88+
const decorated = wrappedOnEvent(Symbol('test.event'));
89+
decorated(mockTarget, 'testMethod', descriptor);
90+
91+
await descriptor.value();
92+
93+
expect(core.startSpan).toHaveBeenCalledWith(
94+
expect.objectContaining({
95+
name: 'event Symbol(test.event)',
96+
}),
97+
expect.any(Function),
98+
);
99+
expect(originalHandler).toHaveBeenCalled();
100+
});
101+
102+
it('should wrap string array event handlers', async () => {
83103
const decorated = wrappedOnEvent(['test.event1', 'test.event2']);
84104
decorated(mockTarget, 'testMethod', descriptor);
85105

86106
await descriptor.value();
87107

88-
expect(core.startSpan).toHaveBeenCalled();
108+
expect(core.startSpan).toHaveBeenCalledWith(
109+
expect.objectContaining({
110+
name: 'event test.event1,test.event2',
111+
}),
112+
expect.any(Function),
113+
);
114+
expect(originalHandler).toHaveBeenCalled();
115+
});
116+
117+
it('should wrap symbol array event handlers', async () => {
118+
const decorated = wrappedOnEvent([Symbol('test.event1'), Symbol('test.event2')]);
119+
decorated(mockTarget, 'testMethod', descriptor);
120+
121+
await descriptor.value();
122+
123+
expect(core.startSpan).toHaveBeenCalledWith(
124+
expect.objectContaining({
125+
name: 'event Symbol(test.event1),Symbol(test.event2)',
126+
}),
127+
expect.any(Function),
128+
);
129+
expect(originalHandler).toHaveBeenCalled();
130+
});
131+
132+
it('should wrap mixed type array event handlers', async () => {
133+
const decorated = wrappedOnEvent([Symbol('test.event1'), 'test.event2', Symbol('test.event3')]);
134+
decorated(mockTarget, 'testMethod', descriptor);
135+
136+
await descriptor.value();
137+
138+
expect(core.startSpan).toHaveBeenCalledWith(
139+
expect.objectContaining({
140+
name: 'event Symbol(test.event1),test.event2,Symbol(test.event3)',
141+
}),
142+
expect.any(Function),
143+
);
89144
expect(originalHandler).toHaveBeenCalled();
90145
});
91146

0 commit comments

Comments
 (0)