Skip to content

Commit 5bf825e

Browse files
committed
fixup
1 parent 199b4e4 commit 5bf825e

4 files changed

Lines changed: 532 additions & 90 deletions

File tree

packages/datadog-instrumentations/src/anthropic.js

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,38 @@ function waitForVerdict (promise, verdict) {
4747
/**
4848
* @param {object} response
4949
* @param {'json'|'text'} method
50+
* @param {object} ctx
5051
* @param {(body: object|string) => Promise<void>|undefined} getVerdict
5152
*/
52-
function wrapResponseReader (response, method, getVerdict) {
53+
function wrapResponseReader (response, method, ctx, getVerdict) {
5354
if (typeof response[method] !== 'function') return
5455

5556
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-
})
57+
return original.apply(this, args)
58+
.then(body => finishResult(ctx, body, getVerdict))
59+
.catch(error => {
60+
if (!ctx.finished) finish(ctx, null, error)
61+
throw error
62+
})
63+
})
64+
}
65+
66+
/**
67+
* @param {object} ctx
68+
* @param {object|string} result
69+
* @param {(body: object|string) => Promise<void>|undefined} getVerdict
70+
* @returns {object|string|Promise<object|string>}
71+
*/
72+
function finishResult (ctx, result, getVerdict) {
73+
const verdict = getVerdict(result)
74+
if (!verdict) {
75+
finish(ctx, result, null)
76+
return result
77+
}
78+
79+
return verdict.then(() => {
80+
finish(ctx, result, null)
81+
return result
6082
})
6183
}
6284

@@ -103,6 +125,7 @@ function wrapCreate (create) {
103125

104126
let apiPromise
105127
try {
128+
// Anthropic starts the request eagerly; the input verdict only gates result delivery.
106129
apiPromise = create.apply(this, args)
107130
} catch (error) {
108131
finish(ctx, null, error)
@@ -143,17 +166,7 @@ function wrapCreate (create) {
143166
shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx))
144167
return response
145168
}
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(() => {
154-
finish(ctx, response, null)
155-
return response
156-
})
169+
return finishResult(ctx, response, getAfterVerdict)
157170
}).catch(error => {
158171
if (!ctx.finished) finish(ctx, null, error)
159172
throw error
@@ -162,19 +175,17 @@ function wrapCreate (create) {
162175
return parseResult
163176
})
164177

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.
178+
// Gate raw access; output evaluation supports only the common json() and text() readers.
167179
shimmer.wrap(apiPromise, 'asResponse', origAsResponse => function (...asResponseArgs) {
168180
return waitForVerdict(origAsResponse.apply(this, asResponseArgs), getBeforeVerdict())
169181
.then(response => {
170-
if (!stream && hasLifecycle && wrappedResponse !== response) {
182+
if (!stream && wrappedResponse !== response) {
171183
wrappedResponse = response
172-
wrapResponseReader(response, 'json', getAfterVerdict)
173-
wrapResponseReader(response, 'text', getAfterVerdict)
184+
wrapResponseReader(response, 'json', ctx, getAfterVerdict)
185+
wrapResponseReader(response, 'text', ctx, getAfterVerdict)
174186
}
175187

176188
if (afterVerdict) return afterVerdict.then(() => response)
177-
if (!stream && !ctx.finished && !parseResult) finish(ctx, null, null)
178189
return response
179190
})
180191
.catch(error => {

packages/datadog-instrumentations/test/anthropic-lifecycle.spec.js

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ describe('anthropic lifecycle instrumentation', () => {
260260
}
261261
})
262262

263-
it('finishes after a resolving before subscriber leaves', async () => {
263+
it('finishes after reading a raw response when the before subscriber leaves', async () => {
264264
const apmChannel = tracingChannel('apm:anthropic:request')
265265
const apmHandlers = { start () {} }
266266
let asyncEndCount = 0
@@ -282,18 +282,19 @@ describe('anthropic lifecycle instrumentation', () => {
282282
messages._nextApiPromise = apiPromise
283283

284284
try {
285-
assert.strictEqual(
286-
await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse(),
287-
apiPromise._rawResponse
288-
)
285+
const response = await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse()
286+
287+
assert.strictEqual(response, apiPromise._rawResponse)
288+
assert.strictEqual(asyncEndCount, 0)
289+
await response.json()
289290
assert.strictEqual(asyncEndCount, 1)
290291
} finally {
291292
apmChannel.unsubscribe(apmHandlers)
292293
unsubscribe()
293294
}
294295
})
295296

296-
it('finishes an unconsumed raw response and evaluates it when read', async () => {
297+
it('finishes a raw response with its result when read', async () => {
297298
const apmChannel = tracingChannel('apm:anthropic:request')
298299
const apmHandlers = { start () {} }
299300
let asyncEndCtx
@@ -312,18 +313,42 @@ describe('anthropic lifecycle instrumentation', () => {
312313
const response = await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse()
313314

314315
assert.strictEqual(calls.length, 1)
315-
assert.ok(asyncEndCtx, 'asyncEnd was not published')
316-
assert.strictEqual(asyncEndCtx.finished, true)
316+
assert.strictEqual(asyncEndCtx, undefined)
317317
assert.strictEqual(response.bodyUsed, false)
318318
assert.deepStrictEqual(await response.json(), body)
319319
assert.strictEqual(calls.length, 2)
320320
assert.deepStrictEqual(calls[1].body, body)
321+
assert.ok(asyncEndCtx, 'asyncEnd was not published')
322+
assert.strictEqual(asyncEndCtx.finished, true)
323+
assert.deepStrictEqual(asyncEndCtx.result, body)
321324
} finally {
322325
apmChannel.unsubscribe(apmHandlers)
323326
unsubscribe()
324327
}
325328
})
326329

330+
it('finishes a raw response reader when only tracing is active', async () => {
331+
const apmChannel = tracingChannel('apm:anthropic:request')
332+
const apmHandlers = { start () {} }
333+
let asyncEndCtx
334+
apmHandlers.asyncEnd = ctx => { asyncEndCtx = ctx }
335+
apmChannel.subscribe(apmHandlers)
336+
337+
const body = { role: 'assistant', content: [{ type: 'text', text: 'Hi' }] }
338+
const messages = new Messages()
339+
messages._nextApiPromise = new FakeAPIPromise(body)
340+
341+
try {
342+
const response = await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse()
343+
344+
assert.strictEqual(asyncEndCtx, undefined)
345+
assert.deepStrictEqual(await response.json(), body)
346+
assert.deepStrictEqual(asyncEndCtx.result, body)
347+
} finally {
348+
apmChannel.unsubscribe(apmHandlers)
349+
}
350+
})
351+
327352
it('evaluates repeated asResponse() calls once', async () => {
328353
const { calls, unsubscribe } = subscribeAutoResolve([
329354
messagesBeforeChannel,
@@ -377,11 +402,12 @@ describe('anthropic lifecycle instrumentation', () => {
377402
)
378403
sinon.assert.notCalled(response.clone)
379404
assert.strictEqual(calls.length, 0)
380-
assert.strictEqual(asyncEndCount, 1)
405+
assert.strictEqual(asyncEndCount, 0)
381406
assert.deepStrictEqual(await response.json(), body)
382407
sinon.assert.calledOnce(readJson)
383408
assert.strictEqual(calls.length, 1)
384409
assert.strictEqual(calls[0].body, body)
410+
assert.strictEqual(asyncEndCount, 1)
385411
} finally {
386412
apmChannel.unsubscribe(apmHandlers)
387413
unsubscribe()
@@ -508,22 +534,33 @@ describe('anthropic lifecycle instrumentation', () => {
508534
}
509535
})
510536

511-
it('keeps a raw-terminal span finished when parsing starts later', async () => {
537+
it('retains the parsed result when parsing starts after raw response access', async () => {
512538
const apmChannel = tracingChannel('apm:anthropic:request')
513539
const apmHandlers = { start () {} }
514540
let asyncEndCount = 0
515-
apmHandlers.asyncEnd = () => { asyncEndCount++ }
541+
let asyncEndCtx
542+
apmHandlers.asyncEnd = ctx => {
543+
asyncEndCount++
544+
asyncEndCtx = ctx
545+
}
516546
apmChannel.subscribe(apmHandlers)
517547

518548
const { unsubscribe } = subscribeAutoResolve([messagesAfterChannel])
549+
const body = {
550+
role: 'assistant',
551+
content: [{ type: 'text', text: 'Hi' }],
552+
usage: { input_tokens: 1, output_tokens: 2 },
553+
}
519554
const messages = new Messages()
520-
messages._nextApiPromise = new FakeAPIPromise({ role: 'assistant', content: [] })
555+
messages._nextApiPromise = new FakeAPIPromise(body)
521556
const apiPromise = messages.create({ messages: [{ role: 'user', content: 'Hi' }] })
522557

523558
try {
524559
await apiPromise.asResponse()
525-
await apiPromise.parse()
560+
assert.strictEqual(asyncEndCount, 0)
561+
assert.strictEqual(await apiPromise.parse(), body)
526562
assert.strictEqual(asyncEndCount, 1)
563+
assert.strictEqual(asyncEndCtx.result, body)
527564
} finally {
528565
apmChannel.unsubscribe(apmHandlers)
529566
unsubscribe()

0 commit comments

Comments
 (0)