@@ -6,6 +6,59 @@ const { addHook } = require('./helpers/instrument')
66
77const anthropicTracingChannel = tracingChannel ( 'apm:anthropic:request' )
88const onStreamedChunkCh = channel ( 'apm:anthropic:request:chunk' )
9+ const messagesBeforeChannel = channel ( 'dd-trace:anthropic:messages:before' )
10+ const messagesAfterChannel = channel ( 'dd-trace:anthropic:messages:after' )
11+
12+ /**
13+ * Publishes a provider-native lifecycle payload to a cancelable lifecycle channel.
14+ *
15+ * Subscribers push async work into `pending` synchronously during publication and
16+ * abort `abortController` with an error before the pushed promise resolves to block.
17+ *
18+ * @param {object } channel
19+ * @param {object } payload
20+ * @returns {Promise<void> }
21+ */
22+ function publishLifecycle ( channel , payload ) {
23+ const abortController = new AbortController ( )
24+ const ctx = { ...payload , abortController, pending : [ ] }
25+
26+ channel . publish ( ctx )
27+
28+ return Promise . all ( ctx . pending ) . then ( ( ) => {
29+ if ( abortController . signal . aborted ) {
30+ throw abortController . signal . reason
31+ }
32+ } )
33+ }
34+
35+ /**
36+ * @template T
37+ * @param {Promise<T> } promise
38+ * @param {Promise<void>|undefined } verdict
39+ * @returns {Promise<T> }
40+ */
41+ function waitForVerdict ( promise , verdict ) {
42+ return verdict
43+ ? Promise . all ( [ verdict , promise ] ) . then ( ( [ , value ] ) => value )
44+ : promise
45+ }
46+
47+ /**
48+ * @param {object } response
49+ * @param {'json'|'text' } method
50+ * @param {(body: object|string) => Promise<void>|undefined } getVerdict
51+ */
52+ function wrapResponseReader ( response , method , getVerdict ) {
53+ if ( typeof response [ method ] !== 'function' ) return
54+
55+ shimmer . wrap ( response , method , original => function ( ...args ) {
56+ return original . apply ( this , args ) . then ( body => {
57+ const verdict = getVerdict ( body )
58+ return verdict ? verdict . then ( ( ) => body ) : body
59+ } )
60+ } )
61+ }
962
1063function wrapStreamIterator ( iterator , ctx ) {
1164 return function ( ...args ) {
@@ -34,16 +87,20 @@ function wrapStreamIterator (iterator, ctx) {
3487
3588function wrapCreate ( create ) {
3689 return function ( ...args ) {
37- if ( ! anthropicTracingChannel . start . hasSubscribers ) {
90+ const options = args [ 0 ]
91+ const stream = options ?. stream
92+
93+ const hasLifecycle = ! stream && ( messagesBeforeChannel . hasSubscribers || messagesAfterChannel . hasSubscribers )
94+
95+ if ( ! anthropicTracingChannel . start . hasSubscribers && ! hasLifecycle ) {
3896 return create . apply ( this , args )
3997 }
4098
41- const options = args [ 0 ]
42- const stream = options . stream
43-
4499 const ctx = { options, resource : 'create' , baseUrl : this . _client ?. baseURL }
45100
46101 return anthropicTracingChannel . start . runStores ( ctx , ( ) => {
102+ const parentSpan = hasLifecycle ? ctx . currentStore ?. span : undefined
103+
47104 let apiPromise
48105 try {
49106 apiPromise = create . apply ( this , args )
@@ -52,18 +109,76 @@ function wrapCreate (create) {
52109 throw error
53110 }
54111
55- shimmer . wrap ( apiPromise , 'parse' , parse => function ( ...args ) {
56- return parse . apply ( this , args )
112+ let afterVerdict
113+ let parseResult
114+ let wrappedResponse
115+
116+ let beforeVerdict
117+ function getBeforeVerdict ( ) {
118+ if ( ! hasLifecycle || beforeVerdict ) return beforeVerdict
119+ if ( ! messagesBeforeChannel . hasSubscribers ) return
120+
121+ beforeVerdict = publishLifecycle ( messagesBeforeChannel , { args, parentSpan } )
122+ return beforeVerdict
123+ }
124+
125+ /**
126+ * @param {object|string } body
127+ */
128+ function getAfterVerdict ( body ) {
129+ if ( ! hasLifecycle || afterVerdict ) return afterVerdict
130+ if ( ! messagesAfterChannel . hasSubscribers ) return
131+
132+ afterVerdict = publishLifecycle ( messagesAfterChannel , { args, body, parentSpan } )
133+ return afterVerdict
134+ }
135+
136+ shimmer . wrap ( apiPromise , 'parse' , parse => function ( ...parseArgs ) {
137+ if ( parseResult ) return parseResult
138+
139+ const parsed = parse . apply ( this , parseArgs )
140+ parseResult = waitForVerdict ( parsed , getBeforeVerdict ( ) )
57141 . then ( response => {
58142 if ( stream ) {
59143 shimmer . wrap ( response , Symbol . asyncIterator , iterator => wrapStreamIterator ( iterator , ctx ) )
60- } else {
144+ return response
145+ }
146+ const verdict = getAfterVerdict ( response )
147+ if ( ! verdict ) {
148+ finish ( ctx , response , null )
149+ return response
150+ }
151+ // Finish after evaluation so a block propagates the error to anthropic.request
152+ // and the span wraps its child instead of closing before it.
153+ return verdict . then ( ( ) => {
61154 finish ( ctx , response , null )
155+ return response
156+ } )
157+ } ) . catch ( error => {
158+ if ( ! ctx . finished ) finish ( ctx , null , error )
159+ throw error
160+ } )
161+
162+ return parseResult
163+ } )
164+
165+ // Gate `.asResponse()` callers on the before verdict so raw-response paths still block,
166+ // then evaluate output only if the caller consumes the JSON body.
167+ shimmer . wrap ( apiPromise , 'asResponse' , origAsResponse => function ( ...asResponseArgs ) {
168+ return waitForVerdict ( origAsResponse . apply ( this , asResponseArgs ) , getBeforeVerdict ( ) )
169+ . then ( response => {
170+ if ( ! stream && hasLifecycle && wrappedResponse !== response ) {
171+ wrappedResponse = response
172+ wrapResponseReader ( response , 'json' , getAfterVerdict )
173+ wrapResponseReader ( response , 'text' , getAfterVerdict )
62174 }
63175
176+ if ( afterVerdict ) return afterVerdict . then ( ( ) => response )
177+ if ( ! stream && ! ctx . finished && ! parseResult ) finish ( ctx , null , null )
64178 return response
65- } ) . catch ( error => {
66- finish ( ctx , null , error )
179+ } )
180+ . catch ( error => {
181+ if ( ! ctx . finished ) finish ( ctx , null , error )
67182 throw error
68183 } )
69184 } )
@@ -76,13 +191,16 @@ function wrapCreate (create) {
76191}
77192
78193function finish ( ctx , result , error ) {
194+ if ( ctx . finished ) return
195+
79196 if ( error ) {
80197 ctx . error = error
81198 anthropicTracingChannel . error . publish ( ctx )
82199 }
83200
84201 // streamed responses are handled and set separately
85202 ctx . result ??= result
203+ ctx . finished = true
86204
87205 anthropicTracingChannel . asyncEnd . publish ( ctx )
88206}
0 commit comments