Skip to content

feat: @google/genai Gemini AI LLM instrumentation #3119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e2eaf2d
google-genai Gemini skeleton
amychisholm03 May 22, 2025
c463a9c
generateContent segment
amychisholm03 May 22, 2025
bd23fcc
generateContentStream and embedContent
amychisholm03 May 22, 2025
733982f
wip: generateContentStreamInternal
amychisholm03 May 22, 2025
7b5bc64
wip: tests
amychisholm03 May 23, 2025
a908467
wip: tests
amychisholm03 May 23, 2025
644a06e
summary test passing
amychisholm03 May 27, 2025
1fd8c60
message test passing
amychisholm03 May 27, 2025
02484b2
tokenCB working
amychisholm03 May 27, 2025
dfe4687
embedding tests passing
amychisholm03 May 27, 2025
9d8ec07
wip: generateContentStreamInternal
amychisholm03 May 27, 2025
c774805
instrumentStream working
amychisholm03 May 27, 2025
721d706
cleanup
amychisholm03 May 27, 2025
5323cdb
concat stream response together
amychisholm03 May 28, 2025
c2217be
no response.headers available
amychisholm03 May 28, 2025
4e1844c
genai.test.js tweak
amychisholm03 May 28, 2025
36914db
comment style
amychisholm03 May 28, 2025
78908ab
wip: test/versioned/google-genai
amychisholm03 May 28, 2025
a8942c2
wip: test/versioned/google-genai
amychisholm03 May 28, 2025
89512c8
unit test fix
amychisholm03 May 28, 2025
d711cb1
cleanup unit tests
amychisholm03 May 29, 2025
f3021c6
fix test client
amychisholm03 May 29, 2025
fb7ed81
wip: test/versioned/google-genai
amychisholm03 May 29, 2025
01ec03a
wip: test/versioned/google-genai
amychisholm03 May 29, 2025
5259671
all versioned tests ok except for streaming
amychisholm03 May 29, 2025
fb21e93
all versioned tests pass
amychisholm03 May 30, 2025
ca16887
get rid of redundant streaming.enabled check
amychisholm03 May 30, 2025
f94ec71
typo
amychisholm03 Jun 2, 2025
e32b9ab
added test for unique google-genai errors
amychisholm03 Jun 2, 2025
b355dfe
tweaks
amychisholm03 Jun 3, 2025
cc97eca
add segment name check
amychisholm03 Jun 3, 2025
2710035
Merge https://github.com/amychisholm03/node-newrelic into NR-2890/gem…
amychisholm03 Jun 3, 2025
e1c33fc
bad json error test
amychisholm03 Jun 3, 2025
eaa4c4c
test names
amychisholm03 Jun 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
all versioned tests pass
  • Loading branch information
amychisholm03 committed May 30, 2025
commit fb21e93e3bc7b4664ccc069f7091544a41ad2ab2
2 changes: 1 addition & 1 deletion lib/instrumentation/@google/genai.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,13 @@

function instrumentStream ({ agent, shim, request, response, segment, transaction }) {
if (!agent.config.ai_monitoring.streaming.enabled) {
shim.logger.warn(
'`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
)
agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
}

Check warning on line 129 in lib/instrumentation/@google/genai.js

View check run for this annotation

Codecov / codecov/patch

lib/instrumentation/@google/genai.js#L125-L129

Added lines #L125 - L129 were not covered by tests

let err = false
let err
let content
let modelVersion
let finishReason
Expand Down Expand Up @@ -190,9 +190,9 @@

module.exports = function initialize(agent, googleGenAi, moduleName, shim) {
if (agent?.config?.ai_monitoring?.enabled !== true) {
shim.logger.debug('config.ai_monitoring.enabled is set to false.')
return
}

Check warning on line 195 in lib/instrumentation/@google/genai.js

View check run for this annotation

Codecov / codecov/patch

lib/instrumentation/@google/genai.js#L193-L195

Added lines #L193 - L195 were not covered by tests

// Update the tracking metric name with the version of the library
// being instrumented. We do not have access to the version when
Expand Down
46 changes: 23 additions & 23 deletions test/versioned/google-genai/chat-completions.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -42,6 +42,7 @@ test.beforeEach(async (ctx) => {
}
})
const { GoogleGenAI } = require('@google/genai')

ctx.nr.client = new GoogleGenAI({
apiKey: 'fake-versioned-test-key',
vertexai: false,
Expand Down Expand Up @@ -138,7 +139,7 @@ test('should create chat completion message and summary for every message sent',

// Streaming tests
test('should create span on successful models generateContentStream', (t, end) => {
const { client, agent, host, port } = t.nr
const { client, agent } = t.nr
helper.runInTransaction(agent, async (tx) => {
const content = 'Streamed response'
const stream = await client.models.generateContentStream({
Expand All @@ -156,13 +157,13 @@ test('should create span on successful models generateContentStream', (t, end) =
assert.equal(chunk.headers, undefined, 'should remove response headers from user result')
assert.equal(chunk.candidates[0].content.role, 'model')
const expectedRes = responses.get(content)
assert.equal(chunk.candidates[0].content.parts[0].text, expectedRes.candidates[0].content.parts[0].text)
assert.equal(chunk.candidates[0].content.parts[0].text, expectedRes.body.candidates[0].content.parts[0].text)
assert.equal(chunk.candidates[0].content.parts[0].text, res)

assertSegments(
tx.trace,
tx.trace.root,
[GEMINI.COMPLETION, [`External/${host}:${port}/chat/completions`]],
[GEMINI.COMPLETION],
{ exact: false }
)

Expand Down Expand Up @@ -197,7 +198,7 @@ test('should create chat completion message and summary for every message sent i
assertChatCompletionMessages({
tx,
chatMsgs,
id: 'chatcmpl-8MzOfSMbLxEy70lYAolSwdCzfguQZ',
id: '0e7e48f05cf962e1692113a49b276e8bb1bc',
model,
resContent: res,
reqContent: content
Expand Down Expand Up @@ -249,7 +250,7 @@ test('should call the tokenCountCallback in streaming', (t, end) => {
tokenUsage: true,
tx,
chatMsgs,
id: 'chatcmpl-8MzOfSMbLxEy70lYAolSwdCzfguQZ',
id: '"0e7e48f05cf962e1692113a49b276e8bb1bc"',
model: expectedModel,
resContent: res,
reqContent: promptContent
Expand All @@ -265,20 +266,22 @@ test('handles error in stream', (t, end) => {
helper.runInTransaction(agent, async (tx) => {
const content = 'bad stream'
const model = 'gemini-2.0-flash'
const stream = await client.models.generateContentStream({
model,
contents: [content, 'What does 1 plus 1 equal?']
})

let res = ''

try {
const stream = await client.models.generateContentStream({
model,
contents: [content, content, content],
config: {
maxOutputTokens: 100,
temperature: 0.5
}
})

for await (const chunk of stream) {
if (chunk.text) res += chunk?.text
// No-op to trigger the error
assert.ok(chunk)
}
} catch (err) {
assert.ok(res)
assert.ok(err.message, 'exceeded count')
} catch {
const events = agent.customEventAggregator.events.toArray()
assert.equal(events.length, 4)
const chatSummary = events.filter(([{ type }]) => type === 'LlmChatCompletionSummary')[0]
Expand All @@ -288,7 +291,7 @@ test('handles error in stream', (t, end) => {
// are asserted in other tests
match(tx.exceptions[0], {
customAttributes: {
'error.message': 'Premature close',
'error.message': /.*bad stream.*/,
completion_id: /\w{32}/
}
})
Expand Down Expand Up @@ -319,17 +322,14 @@ test('should not create llm events when ai_monitoring.streaming.enabled is false
let chunk = {}

for await (chunk of stream) {
res += chunk?.text
assert.ok(chunk.text, 'should have text in chunk')
res += chunk.text
}
const expectedRes = responses.get(content)
assert.equal(res, expectedRes.streamData)
assert.equal(res, expectedRes.body.candidates[0].content.parts[0].text)

const events = agent.customEventAggregator.events.toArray()
assert.equal(events.length, 0, 'should not llm events when streaming is disabled')
const metrics = agent.metrics.getOrCreateMetric(TRACKING_METRIC)
assert.equal(metrics.callCount > 0, true)
const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_EVENT)
assert.equal(attributes.llm, true)
const streamingDisabled = agent.metrics.getOrCreateMetric(
'Supportability/Nodejs/ML/Streaming/Disabled'
)
Expand Down
6 changes: 3 additions & 3 deletions test/versioned/google-genai/common.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 New Relic Corporation. All rights reserved.
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -72,13 +72,13 @@ function assertChatCompletionSummary(
appName: 'New Relic for Node.js tests',
trace_id: tx.traceId,
span_id: segment.id,
'response.model': model,
'response.model': error ? undefined : model,
vendor: 'gemini',
ingest_source: 'Node',
'request.model': model,
duration: segment.getDurationInMillis(),
'response.number_of_messages': 3,
'response.choices.finish_reason': 'STOP',
'response.choices.finish_reason': error ? undefined : 'STOP',
'request.max_tokens': 100,
'request.temperature': 0.5,
error
Expand Down
2 changes: 1 addition & 1 deletion test/versioned/google-genai/embeddings.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down
2 changes: 1 addition & 1 deletion test/versioned/google-genai/feedback-messages.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* Copyright 2025 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down
18 changes: 10 additions & 8 deletions test/versioned/google-genai/mock-responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,35 +92,37 @@ responses.set('Embedding not allowed.', {
responses.set('Streamed response', {
code: 200,
body: {
modelVersion: 'gemini-2.0-flash',
candidates: [
{
content: {
parts: [
{ text: "A streamed response is a way of transmitting data from a server to a client (e.g. from a website to a user's computer or mobile device) in a continuous flow or stream, rather than all at one time. This means the client can start to process the data before all of it has been received, which can improve performance for large amounts of data or slow connections. Streaming is often used for real-time or near-real-time applications like video or audio playback." }
]
],
role: 'model'
},
role: 'model',
usageMetadata: { promptTokenCount: 999, totalTokenCount: 999, promptTokensDetails: [{ modality: 'TEXT', tokenCount: 999 }] },
finishReason: 'STOP'
}
],
finish_reason: 'STOP'
modelVersion: 'gemini-2.0-flash'
}
})

responses.set('bad stream', {
code: 200,
body: {
modelVersion: 'gemini-2.0-flash',
candidates: [
{
content: {
parts: [
{ text: 'do random' }
]
],
role: 'model'
},
role: 'model',
usageMetadata: { promptTokenCount: 2, totalTokenCount: 2, promptTokensDetails: [{ modality: 'TEXT', tokenCount: 2 }] },
finishReason: 'STOP'
}
],
finish_reason: 'STOP'
modelVersion: 'gemini-2.0-flash'
}
})
28 changes: 25 additions & 3 deletions test/versioned/google-genai/mock-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,32 @@ function handler(req, res) {

const { code, body } = RESPONSES.get(prompt)
res.statusCode = code
res.write(JSON.stringify(body))
res.end()

// TODO: Mock streaming responses
if (prompt.toLowerCase().includes('stream')) {
res.setHeader('Content-Type', 'application/json')
res.setHeader('Transfer-Encoding', 'chunked')

// Simulate streaming chunks
const streamData = body

// SSE format: data: {json}\r\n\r\n
if (prompt.toLowerCase().includes('bad')) {
const errorObj = {
error: {
status: 'INTERNAL',
code: 500,
message: 'bad stream'
}
}
res.write(JSON.stringify(errorObj))
} else res.write('data: ' + JSON.stringify(streamData) + '\r\n\r\n')

// Do not write any extra data after the last chunk
res.end()
} else {
res.write(JSON.stringify(body))
res.end()
}
})
}

Expand Down
4 changes: 2 additions & 2 deletions test/versioned/google-genai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"version": "0.0.0",
"private": true,
"engines": {
"node": ">=20"
"node": ">=18"
},
"tests": [
{
"engines": {
"node": ">=20"
"node": ">=18"
},
"dependencies": {
"@google/genai": "^1.2.0"
Expand Down
Loading