Skip to content

Commit d828f59

Browse files
authored
Merge branch 'develop' into msgs-drop-media
2 parents 28bfcba + 9b584f2 commit d828f59

File tree

8 files changed

+250
-17
lines changed

8 files changed

+250
-17
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,
@@ -135,6 +136,8 @@ function processEndedVercelAiSpan(span: SpanJSON): void {
135136
renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input');
136137
renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output');
137138

139+
renameAttributeKey(attributes, AI_SCHEMA_ATTRIBUTE, 'gen_ai.request.schema');
140+
138141
addProviderMetadataToAttributes(attributes);
139142

140143
// 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

packages/react-router/src/server/createSentryHandleRequest.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ export interface SentryHandleRequestOptions {
5353
botRegex?: RegExp;
5454
}
5555

56+
type HandleRequestWithoutMiddleware = (
57+
request: Request,
58+
responseStatusCode: number,
59+
responseHeaders: Headers,
60+
routerContext: EntryContext,
61+
loadContext: AppLoadContext,
62+
) => Promise<unknown>;
63+
64+
type HandleRequestWithMiddleware = (
65+
request: Request,
66+
responseStatusCode: number,
67+
responseHeaders: Headers,
68+
routerContext: EntryContext,
69+
loadContext: RouterContextProvider,
70+
) => Promise<unknown>;
71+
5672
/**
5773
* A complete Sentry-instrumented handleRequest implementation that handles both
5874
* route parametrization and trace meta tag injection.
@@ -62,13 +78,7 @@ export interface SentryHandleRequestOptions {
6278
*/
6379
export function createSentryHandleRequest(
6480
options: SentryHandleRequestOptions,
65-
): (
66-
request: Request,
67-
responseStatusCode: number,
68-
responseHeaders: Headers,
69-
routerContext: EntryContext,
70-
loadContext: AppLoadContext | RouterContextProvider,
71-
) => Promise<unknown> {
81+
): HandleRequestWithoutMiddleware & HandleRequestWithMiddleware {
7282
const {
7383
streamTimeout = 10000,
7484
renderToPipeableStream,
@@ -135,5 +145,6 @@ export function createSentryHandleRequest(
135145
};
136146

137147
// Wrap the handle request function for request parametrization
138-
return wrapSentryHandleRequest(handleRequest);
148+
return wrapSentryHandleRequest(handleRequest as HandleRequestWithoutMiddleware) as HandleRequestWithoutMiddleware &
149+
HandleRequestWithMiddleware;
139150
}

packages/react-router/src/server/wrapSentryHandleRequest.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ import {
1010
} from '@sentry/core';
1111
import type { AppLoadContext, EntryContext, RouterContextProvider } from 'react-router';
1212

13-
type OriginalHandleRequest = (
13+
type OriginalHandleRequestWithoutMiddleware = (
1414
request: Request,
1515
responseStatusCode: number,
1616
responseHeaders: Headers,
1717
routerContext: EntryContext,
18-
loadContext: AppLoadContext | RouterContextProvider,
18+
loadContext: AppLoadContext,
19+
) => Promise<unknown>;
20+
21+
type OriginalHandleRequestWithMiddleware = (
22+
request: Request,
23+
responseStatusCode: number,
24+
responseHeaders: Headers,
25+
routerContext: EntryContext,
26+
loadContext: RouterContextProvider,
1927
) => Promise<unknown>;
2028

2129
/**
@@ -24,7 +32,27 @@ type OriginalHandleRequest = (
2432
* @param originalHandle - The original handleRequest function to wrap
2533
* @returns A wrapped version of the handle request function with Sentry instrumentation
2634
*/
27-
export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest): OriginalHandleRequest {
35+
export function wrapSentryHandleRequest(
36+
originalHandle: OriginalHandleRequestWithoutMiddleware,
37+
): OriginalHandleRequestWithoutMiddleware;
38+
/**
39+
* Wraps the original handleRequest function to add Sentry instrumentation.
40+
*
41+
* @param originalHandle - The original handleRequest function to wrap
42+
* @returns A wrapped version of the handle request function with Sentry instrumentation
43+
*/
44+
export function wrapSentryHandleRequest(
45+
originalHandle: OriginalHandleRequestWithMiddleware,
46+
): OriginalHandleRequestWithMiddleware;
47+
/**
48+
* Wraps the original handleRequest function to add Sentry instrumentation.
49+
*
50+
* @param originalHandle - The original handleRequest function to wrap
51+
* @returns A wrapped version of the handle request function with Sentry instrumentation
52+
*/
53+
export function wrapSentryHandleRequest(
54+
originalHandle: OriginalHandleRequestWithoutMiddleware | OriginalHandleRequestWithMiddleware,
55+
): OriginalHandleRequestWithoutMiddleware | OriginalHandleRequestWithMiddleware {
2856
return async function sentryInstrumentedHandleRequest(
2957
request: Request,
3058
responseStatusCode: number,
@@ -57,10 +85,39 @@ export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest):
5785
}
5886

5987
try {
60-
return await originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext);
88+
// Type guard to call the correct overload based on loadContext type
89+
if (isRouterContextProvider(loadContext)) {
90+
// loadContext is RouterContextProvider
91+
return await (originalHandle as OriginalHandleRequestWithMiddleware)(
92+
request,
93+
responseStatusCode,
94+
responseHeaders,
95+
routerContext,
96+
loadContext,
97+
);
98+
} else {
99+
// loadContext is AppLoadContext
100+
return await (originalHandle as OriginalHandleRequestWithoutMiddleware)(
101+
request,
102+
responseStatusCode,
103+
responseHeaders,
104+
routerContext,
105+
loadContext,
106+
);
107+
}
61108
} finally {
62109
await flushIfServerless();
63110
}
111+
112+
/**
113+
* Helper type guard to determine if the context is a RouterContextProvider.
114+
*
115+
* @param ctx - The context to check
116+
* @returns True if the context is a RouterContextProvider
117+
*/
118+
function isRouterContextProvider(ctx: AppLoadContext | RouterContextProvider): ctx is RouterContextProvider {
119+
return typeof (ctx as RouterContextProvider)?.get === 'function';
120+
}
64121
};
65122
}
66123

0 commit comments

Comments
 (0)