@@ -36,10 +36,9 @@ function withAiSdkOpenAiVersions (versionRange, callback) {
3636 } )
3737}
3838
39- // making a different reference from the default no-op tracer in the instrumentation
40- // attempted to use the DD tracer provider, but it double-traces the request
41- // in practice, there is no need to pass in the DD OTel tracer provider, so this
42- // case shouldn't be an issue in practice
39+ // Use a distinct tracer without creating additional dd-trace spans in tests that
40+ // only exercise custom-tracer behavior. The error-path regression below uses the
41+ // real dd-trace OTel provider because its private span fields are part of the bug.
4342const myTracer = {
4443 startActiveSpan ( ) {
4544 const fn = arguments [ arguments . length - 1 ]
@@ -86,6 +85,40 @@ describe('Plugin', () => {
8685 } )
8786
8887 describe ( 'patching behavior with experimental_telemetry options' , ( ) => {
88+ if ( semifies ( realVersion , '>=6.0.0' ) ) {
89+ it ( 'preserves the original model error with a dd-trace OTel tracer' , async ( ) => {
90+ const originalError = new Error ( 'original model error' )
91+ const model = {
92+ specificationVersion : 'v3' ,
93+ provider : 'test' ,
94+ modelId : 'test' ,
95+ supportedUrls : { } ,
96+ doGenerate ( ) { return Promise . reject ( originalError ) } ,
97+ doStream ( ) { return Promise . reject ( originalError ) } ,
98+ }
99+ const TracerProvider = require ( '../../dd-trace/src/opentelemetry/tracer_provider' )
100+ const otelTracer = new TracerProvider ( ) . getTracer ( 'ai' )
101+ const checkTraces = agent . assertSomeTraces ( traces => {
102+ const errorSpan = traces . flat ( ) . find ( span => span . meta ?. [ 'error.message' ] === originalError . message )
103+
104+ assert . ok ( errorSpan , 'Expected a span for the original model error' )
105+ assert . strictEqual ( errorSpan . error , 1 )
106+ } )
107+
108+ await assert . rejects (
109+ ai . generateText ( {
110+ model,
111+ prompt : 'trigger an error' ,
112+ maxRetries : 0 ,
113+ experimental_telemetry : { isEnabled : true , tracer : otelTracer } ,
114+ } ) ,
115+ error => error === originalError
116+ )
117+
118+ await checkTraces
119+ } )
120+ }
121+
89122 it ( 'should not error when `isEnabled` is false' , async ( ) => {
90123 const experimentalTelemetry = { isEnabled : false }
91124 const result = await ai . generateText ( {
@@ -223,6 +256,89 @@ describe('Plugin', () => {
223256 assert . ok ( result . text , 'Expected result to be truthy' )
224257 } )
225258
259+ if ( semifies ( realVersion , '>=6.0.0' ) ) {
260+ it ( 'delegates the complete span interface to the original span' , async ( ) => {
261+ const calls = [ ]
262+ const context = { traceId : '0' . repeat ( 32 ) , spanId : '0' . repeat ( 16 ) , traceFlags : 1 }
263+ const originalError = new Error ( 'model error' )
264+ class RecordingSpan {
265+ // eslint-disable-next-line no-unused-private-class-members
266+ #statusCode = 0
267+
268+ spanContext ( ) { calls . push ( [ 'spanContext' , this , [ ] ] ) ; return context }
269+ setAttribute ( ...args ) { calls . push ( [ 'setAttribute' , this , args ] ) ; return this }
270+ setAttributes ( ...args ) { calls . push ( [ 'setAttributes' , this , args ] ) ; return this }
271+ addEvent ( ...args ) { calls . push ( [ 'addEvent' , this , args ] ) ; return this }
272+ addLink ( ...args ) { calls . push ( [ 'addLink' , this , args ] ) ; return this }
273+ addLinks ( ...args ) { calls . push ( [ 'addLinks' , this , args ] ) ; return this }
274+ setStatus ( ...args ) { this . #statusCode = args [ 0 ] . code ; calls . push ( [ 'setStatus' , this , args ] ) ; return this }
275+ updateName ( ...args ) { calls . push ( [ 'updateName' , this , args ] ) ; return this }
276+ end ( ...args ) { calls . push ( [ 'end' , this , args ] ) }
277+ isRecording ( ) { calls . push ( [ 'isRecording' , this , [ ] ] ) ; return true }
278+ recordException ( ...args ) { calls . push ( [ 'recordException' , this , args ] ) }
279+ }
280+
281+ const originalSpan = new RecordingSpan ( )
282+ const tracer = {
283+ startActiveSpan ( ...args ) {
284+ const fn = args [ args . length - 1 ]
285+ return fn ( originalSpan )
286+ } ,
287+ }
288+
289+ await assert . rejects (
290+ ai . generateText ( {
291+ model : {
292+ specificationVersion : 'v3' ,
293+ provider : 'test' ,
294+ modelId : 'test' ,
295+ supportedUrls : { } ,
296+ doGenerate ( ) { return Promise . reject ( originalError ) } ,
297+ doStream ( ) { return Promise . reject ( originalError ) } ,
298+ } ,
299+ prompt : 'trigger an error' ,
300+ maxRetries : 0 ,
301+ experimental_telemetry : { isEnabled : true , tracer } ,
302+ } ) ,
303+ error => error === originalError
304+ )
305+
306+ calls . length = 0
307+ const delegatedSpan = tracer . startActiveSpan ( 'delegation-check' , span => span )
308+ const attributes = { key : 'value' }
309+ const eventAttributes = { event : 'value' }
310+ const exception = new Error ( 'delegated error' )
311+ const link = { context }
312+ const links = [ link ]
313+
314+ assert . strictEqual ( delegatedSpan . spanContext ( ) , context )
315+ assert . strictEqual ( delegatedSpan . setAttribute ( 'key' , 'value' ) , delegatedSpan )
316+ assert . strictEqual ( delegatedSpan . setAttributes ( attributes ) , delegatedSpan )
317+ assert . strictEqual ( delegatedSpan . addEvent ( 'event' , eventAttributes , 123 ) , delegatedSpan )
318+ assert . strictEqual ( delegatedSpan . addLink ( link ) , delegatedSpan )
319+ assert . strictEqual ( delegatedSpan . addLinks ( links ) , delegatedSpan )
320+ assert . strictEqual ( delegatedSpan . setStatus ( { code : 1 } ) , delegatedSpan )
321+ assert . strictEqual ( delegatedSpan . updateName ( 'renamed' ) , delegatedSpan )
322+ assert . strictEqual ( delegatedSpan . isRecording ( ) , true )
323+ assert . strictEqual ( delegatedSpan . recordException ( exception , 456 ) , undefined )
324+ delegatedSpan . end ( 123 )
325+
326+ assert . deepStrictEqual ( calls , [
327+ [ 'spanContext' , originalSpan , [ ] ] ,
328+ [ 'setAttribute' , originalSpan , [ 'key' , 'value' ] ] ,
329+ [ 'setAttributes' , originalSpan , [ attributes ] ] ,
330+ [ 'addEvent' , originalSpan , [ 'event' , eventAttributes , 123 ] ] ,
331+ [ 'addLink' , originalSpan , [ link ] ] ,
332+ [ 'addLinks' , originalSpan , [ links ] ] ,
333+ [ 'setStatus' , originalSpan , [ { code : 1 } ] ] ,
334+ [ 'updateName' , originalSpan , [ 'renamed' ] ] ,
335+ [ 'isRecording' , originalSpan , [ ] ] ,
336+ [ 'recordException' , originalSpan , [ exception , 456 ] ] ,
337+ [ 'end' , originalSpan , [ 123 ] ] ,
338+ ] )
339+ } )
340+ }
341+
226342 it ( 'should use the passed in `tracer`' , async ( ) => {
227343 const checkTraces = agent . assertSomeTraces ( traces => {
228344 const generateTextSpan = traces [ 0 ] [ 0 ]
0 commit comments