google-logging-utils-internal 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/LICENSE +203 -0
- package/package/README.md +83 -0
- package/package/lib/auth.d.mts +33 -0
- package/package/lib/auth.d.ts +33 -0
- package/package/lib/auth.js +70 -0
- package/package/lib/auth.js.map +1 -0
- package/package/lib/auth.mjs +45 -0
- package/package/lib/auth.mjs.map +1 -0
- package/package/lib/gcpLogger.d.mts +25 -0
- package/package/lib/gcpLogger.d.ts +25 -0
- package/package/lib/gcpLogger.js +118 -0
- package/package/lib/gcpLogger.js.map +1 -0
- package/package/lib/gcpLogger.mjs +82 -0
- package/package/lib/gcpLogger.mjs.map +1 -0
- package/package/lib/gcpOpenTelemetry.d.mts +59 -0
- package/package/lib/gcpOpenTelemetry.d.ts +59 -0
- package/package/lib/gcpOpenTelemetry.js +374 -0
- package/package/lib/gcpOpenTelemetry.js.map +1 -0
- package/package/lib/gcpOpenTelemetry.mjs +364 -0
- package/package/lib/gcpOpenTelemetry.mjs.map +1 -0
- package/package/lib/index.d.mts +36 -0
- package/package/lib/index.d.ts +36 -0
- package/package/lib/index.js +56 -0
- package/package/lib/index.js.map +1 -0
- package/package/lib/index.mjs +29 -0
- package/package/lib/index.mjs.map +1 -0
- package/package/lib/metrics.d.mts +65 -0
- package/package/lib/metrics.d.ts +65 -0
- package/package/lib/metrics.js +91 -0
- package/package/lib/metrics.js.map +1 -0
- package/package/lib/metrics.mjs +65 -0
- package/package/lib/metrics.mjs.map +1 -0
- package/package/lib/model-armor.d.mts +59 -0
- package/package/lib/model-armor.d.ts +59 -0
- package/package/lib/model-armor.js +205 -0
- package/package/lib/model-armor.js.map +1 -0
- package/package/lib/model-armor.mjs +181 -0
- package/package/lib/model-armor.mjs.map +1 -0
- package/package/lib/telemetry/action.d.mts +27 -0
- package/package/lib/telemetry/action.d.ts +27 -0
- package/package/lib/telemetry/action.js +92 -0
- package/package/lib/telemetry/action.js.map +1 -0
- package/package/lib/telemetry/action.mjs +73 -0
- package/package/lib/telemetry/action.mjs.map +1 -0
- package/package/lib/telemetry/defaults.d.mts +30 -0
- package/package/lib/telemetry/defaults.d.ts +30 -0
- package/package/lib/telemetry/defaults.js +70 -0
- package/package/lib/telemetry/defaults.js.map +1 -0
- package/package/lib/telemetry/defaults.mjs +46 -0
- package/package/lib/telemetry/defaults.mjs.map +1 -0
- package/package/lib/telemetry/engagement.d.mts +35 -0
- package/package/lib/telemetry/engagement.d.ts +35 -0
- package/package/lib/telemetry/engagement.js +106 -0
- package/package/lib/telemetry/engagement.js.map +1 -0
- package/package/lib/telemetry/engagement.mjs +85 -0
- package/package/lib/telemetry/engagement.mjs.map +1 -0
- package/package/lib/telemetry/feature.d.mts +35 -0
- package/package/lib/telemetry/feature.d.ts +35 -0
- package/package/lib/telemetry/feature.js +142 -0
- package/package/lib/telemetry/feature.js.map +1 -0
- package/package/lib/telemetry/feature.mjs +127 -0
- package/package/lib/telemetry/feature.mjs.map +1 -0
- package/package/lib/telemetry/generate.d.mts +53 -0
- package/package/lib/telemetry/generate.d.ts +53 -0
- package/package/lib/telemetry/generate.js +326 -0
- package/package/lib/telemetry/generate.js.map +1 -0
- package/package/lib/telemetry/generate.mjs +314 -0
- package/package/lib/telemetry/generate.mjs.map +1 -0
- package/package/lib/telemetry/path.d.mts +32 -0
- package/package/lib/telemetry/path.d.ts +32 -0
- package/package/lib/telemetry/path.js +91 -0
- package/package/lib/telemetry/path.js.map +1 -0
- package/package/lib/telemetry/path.mjs +78 -0
- package/package/lib/telemetry/path.mjs.map +1 -0
- package/package/lib/types.d.mts +121 -0
- package/package/lib/types.d.ts +121 -0
- package/package/lib/types.js +17 -0
- package/package/lib/types.js.map +1 -0
- package/package/lib/types.mjs +1 -0
- package/package/lib/types.mjs.map +1 -0
- package/package/lib/utils.d.mts +57 -0
- package/package/lib/utils.d.ts +57 -0
- package/package/lib/utils.js +143 -0
- package/package/lib/utils.js.map +1 -0
- package/package/lib/utils.mjs +104 -0
- package/package/lib/utils.mjs.map +1 -0
- package/package/package.json +90 -0
- package/package/src/auth.ts +89 -0
- package/package/src/gcpLogger.ts +124 -0
- package/package/src/gcpOpenTelemetry.ts +485 -0
- package/package/src/index.ts +59 -0
- package/package/src/metrics.ts +122 -0
- package/package/src/model-armor.ts +317 -0
- package/package/src/telemetry/action.ts +106 -0
- package/package/src/telemetry/defaults.ts +72 -0
- package/package/src/telemetry/engagement.ts +120 -0
- package/package/src/telemetry/feature.ts +170 -0
- package/package/src/telemetry/generate.ts +454 -0
- package/package/src/telemetry/path.ts +111 -0
- package/package/src/types.ts +133 -0
- package/package/src/utils.ts +175 -0
- package/package/tests/logs_no_input_output_test.ts +267 -0
- package/package/tests/logs_session_test.ts +219 -0
- package/package/tests/logs_test.ts +633 -0
- package/package/tests/metrics_test.ts +792 -0
- package/package/tests/model_armor_test.ts +336 -0
- package/package/tests/traces_test.ts +380 -0
- package/package/typedoc.json +3 -0
- package/package.json +10 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2025 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { beforeEach, describe, expect, it } from '@jest/globals';
|
|
18
|
+
import { genkit } from 'genkit';
|
|
19
|
+
import { modelArmor } from '../src/model-armor.js';
|
|
20
|
+
|
|
21
|
+
// Mock ModelArmorClient
|
|
22
|
+
class MockModelArmorClient {
|
|
23
|
+
sanitizeUserPrompt = async () => [{}];
|
|
24
|
+
sanitizeModelResponse = async () => [{}];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createEmptyResult() {
|
|
28
|
+
return { sanitizationResult: {} };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createSdpResult(replacementText: string) {
|
|
32
|
+
return {
|
|
33
|
+
sanitizationResult: {
|
|
34
|
+
filterResults: {
|
|
35
|
+
sdp: {
|
|
36
|
+
sdpFilterResult: {
|
|
37
|
+
deidentifyResult: {
|
|
38
|
+
data: { text: replacementText },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createRaiBlockResult() {
|
|
48
|
+
return {
|
|
49
|
+
sanitizationResult: {
|
|
50
|
+
filterMatchState: 'MATCH_FOUND',
|
|
51
|
+
filterResults: {
|
|
52
|
+
rai: { raiFilterResult: { matchState: 'MATCH_FOUND' } },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createSdpBlockResult(replacementText: string) {
|
|
59
|
+
const res = createSdpResult(replacementText);
|
|
60
|
+
(res.sanitizationResult as any).filterMatchState = 'MATCH_FOUND';
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('modelArmor', () => {
|
|
65
|
+
let ai: any;
|
|
66
|
+
let mockClient: any;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
ai = genkit({});
|
|
70
|
+
ai.defineModel({ name: 'echoModel' }, async (req: any) => {
|
|
71
|
+
return {
|
|
72
|
+
message: {
|
|
73
|
+
role: 'model',
|
|
74
|
+
content: [{ text: `Echo: ${req.messages[0].content[0].text}` }],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
mockClient = new MockModelArmorClient();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('passes through when no sanitization triggers', async () => {
|
|
82
|
+
mockClient.sanitizeUserPrompt = async () => [createEmptyResult()];
|
|
83
|
+
mockClient.sanitizeModelResponse = async () => [createEmptyResult()];
|
|
84
|
+
|
|
85
|
+
const response = await ai.generate({
|
|
86
|
+
model: 'echoModel',
|
|
87
|
+
prompt: 'hello',
|
|
88
|
+
use: [modelArmor({ templateName: 'test', client: mockClient as any })],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(response.text).toMatch(/Echo: hello/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('replaces user prompt on SDP match', async () => {
|
|
95
|
+
mockClient.sanitizeUserPrompt = async () => [
|
|
96
|
+
createSdpResult('sanitized_hello'),
|
|
97
|
+
];
|
|
98
|
+
mockClient.sanitizeModelResponse = async () => [createEmptyResult()];
|
|
99
|
+
|
|
100
|
+
const response = await ai.generate({
|
|
101
|
+
model: 'echoModel',
|
|
102
|
+
prompt: 'hello',
|
|
103
|
+
use: [
|
|
104
|
+
modelArmor({
|
|
105
|
+
templateName: 'test',
|
|
106
|
+
client: mockClient as any,
|
|
107
|
+
applyDeidentificationResults: true,
|
|
108
|
+
}),
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// The echo model should receive the SANITIZED prompt
|
|
113
|
+
expect(response.text).toMatch(/Echo: sanitized_hello/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('blocks user prompt on RAI match', async () => {
|
|
117
|
+
mockClient.sanitizeUserPrompt = async () => [createRaiBlockResult()];
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
ai.generate({
|
|
121
|
+
model: 'echoModel',
|
|
122
|
+
prompt: 'bad stuff',
|
|
123
|
+
use: [modelArmor({ templateName: 'test', client: mockClient as any })],
|
|
124
|
+
})
|
|
125
|
+
).rejects.toThrow(
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
status: 'PERMISSION_DENIED',
|
|
128
|
+
message: expect.stringContaining('Model Armor blocked user prompt.'),
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('replaces model response on SDP match', async () => {
|
|
134
|
+
mockClient.sanitizeUserPrompt = async () => [createEmptyResult()];
|
|
135
|
+
mockClient.sanitizeModelResponse = async () => [
|
|
136
|
+
createSdpResult('sanitized_response'),
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const response = await ai.generate({
|
|
140
|
+
model: 'echoModel',
|
|
141
|
+
prompt: 'hello',
|
|
142
|
+
use: [
|
|
143
|
+
modelArmor({
|
|
144
|
+
templateName: 'test',
|
|
145
|
+
client: mockClient as any,
|
|
146
|
+
applyDeidentificationResults: true,
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(response.text).toBe('sanitized_response');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('blocks model response on RAI match', async () => {
|
|
155
|
+
mockClient.sanitizeUserPrompt = async () => [createEmptyResult()];
|
|
156
|
+
mockClient.sanitizeModelResponse = async () => [createRaiBlockResult()];
|
|
157
|
+
|
|
158
|
+
await expect(
|
|
159
|
+
ai.generate({
|
|
160
|
+
model: 'echoModel',
|
|
161
|
+
prompt: 'hello',
|
|
162
|
+
use: [modelArmor({ templateName: 'test', client: mockClient as any })],
|
|
163
|
+
})
|
|
164
|
+
).rejects.toThrow(
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
status: 'PERMISSION_DENIED',
|
|
167
|
+
message: expect.stringContaining('Model Armor blocked model response.'),
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('respects protectionTarget=userPrompt', async () => {
|
|
173
|
+
// Should sanitize prompt but NOT response
|
|
174
|
+
mockClient.sanitizeUserPrompt = async () => [createSdpResult('sanitized')];
|
|
175
|
+
// This one would block if called
|
|
176
|
+
mockClient.sanitizeModelResponse = async () => [createRaiBlockResult()];
|
|
177
|
+
|
|
178
|
+
const response = await ai.generate({
|
|
179
|
+
model: 'echoModel',
|
|
180
|
+
prompt: 'hello',
|
|
181
|
+
use: [
|
|
182
|
+
modelArmor({
|
|
183
|
+
templateName: 'test',
|
|
184
|
+
client: mockClient as any,
|
|
185
|
+
protectionTarget: 'userPrompt',
|
|
186
|
+
applyDeidentificationResults: true,
|
|
187
|
+
}),
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(response.text).toMatch(/Echo: sanitized/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('strictSdpEnforcement blocks even if remediated', async () => {
|
|
195
|
+
mockClient.sanitizeUserPrompt = async () => [
|
|
196
|
+
createSdpBlockResult('sanitized_hello'),
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
ai.generate({
|
|
201
|
+
model: 'echoModel',
|
|
202
|
+
prompt: 'sensitive',
|
|
203
|
+
use: [
|
|
204
|
+
modelArmor({
|
|
205
|
+
templateName: 'test',
|
|
206
|
+
client: mockClient as any,
|
|
207
|
+
strictSdpEnforcement: true,
|
|
208
|
+
applyDeidentificationResults: true,
|
|
209
|
+
}),
|
|
210
|
+
],
|
|
211
|
+
})
|
|
212
|
+
).rejects.toThrow(
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
status: 'PERMISSION_DENIED',
|
|
215
|
+
message: expect.stringContaining('Model Armor blocked user prompt.'),
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('respects filters option', async () => {
|
|
221
|
+
// RAI match found, but we only filter 'csam'
|
|
222
|
+
mockClient.sanitizeUserPrompt = async () => [
|
|
223
|
+
{
|
|
224
|
+
sanitizationResult: {
|
|
225
|
+
filterMatchState: 'MATCH_FOUND',
|
|
226
|
+
filterResults: {
|
|
227
|
+
rai: { raiFilterResult: { matchState: 'MATCH_FOUND' } },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
const response = await ai.generate({
|
|
234
|
+
model: 'echoModel',
|
|
235
|
+
prompt: 'bad stuff',
|
|
236
|
+
use: [
|
|
237
|
+
modelArmor({
|
|
238
|
+
templateName: 'test',
|
|
239
|
+
client: mockClient as any,
|
|
240
|
+
filters: ['csam'],
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(response.text).toMatch(/Echo: bad stuff/);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('preserves non-text parts when SDP replaces text', async () => {
|
|
249
|
+
mockClient.sanitizeUserPrompt = async () => [
|
|
250
|
+
createSdpResult('sanitized_text'),
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
ai.defineModel({ name: 'inspectionModel' }, async (req: any) => {
|
|
254
|
+
return {
|
|
255
|
+
message: {
|
|
256
|
+
role: 'model',
|
|
257
|
+
content: [{ text: JSON.stringify(req.messages) }],
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const response = await ai.generate({
|
|
263
|
+
model: 'inspectionModel',
|
|
264
|
+
messages: [
|
|
265
|
+
{
|
|
266
|
+
role: 'user',
|
|
267
|
+
content: [{ text: 'old stuff' }],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
role: 'model',
|
|
271
|
+
content: [{ text: 'response' }],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
role: 'user',
|
|
275
|
+
content: [
|
|
276
|
+
{ text: 'sensitive info' },
|
|
277
|
+
{ media: { url: 'http://example.com/image.png' } },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
use: [
|
|
282
|
+
modelArmor({
|
|
283
|
+
templateName: 'test',
|
|
284
|
+
client: mockClient as any,
|
|
285
|
+
applyDeidentificationResults: true,
|
|
286
|
+
}),
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const content = JSON.parse(response.text);
|
|
291
|
+
// content should have preserved media and replaced text
|
|
292
|
+
expect(content).toEqual([
|
|
293
|
+
{
|
|
294
|
+
role: 'user',
|
|
295
|
+
content: [{ text: 'old stuff' }],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
role: 'model',
|
|
299
|
+
content: [{ text: 'response' }],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
role: 'user',
|
|
303
|
+
content: [
|
|
304
|
+
{ media: { url: 'http://example.com/image.png' } },
|
|
305
|
+
{ text: 'sanitized_text' },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('supports custom function for applying SDP', async () => {
|
|
312
|
+
mockClient.sanitizeUserPrompt = async () => [
|
|
313
|
+
createSdpResult('sanitized_text'),
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
const applyFn = ({ messages, sdpResult }: any) => {
|
|
317
|
+
// Custom logic: replace with "CUSTOM APPLIED" instead of sdpResult data
|
|
318
|
+
const newContent = [{ text: 'CUSTOM APPLIED' }];
|
|
319
|
+
return [{ ...messages[0], content: newContent }];
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const response = await ai.generate({
|
|
323
|
+
model: 'echoModel',
|
|
324
|
+
prompt: 'hello',
|
|
325
|
+
use: [
|
|
326
|
+
modelArmor({
|
|
327
|
+
templateName: 'test',
|
|
328
|
+
client: mockClient as any,
|
|
329
|
+
applyDeidentificationResults: applyFn,
|
|
330
|
+
}),
|
|
331
|
+
],
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(response.text).toMatch(/Echo: CUSTOM APPLIED/);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
afterAll,
|
|
19
|
+
beforeAll,
|
|
20
|
+
beforeEach,
|
|
21
|
+
describe,
|
|
22
|
+
expect,
|
|
23
|
+
it,
|
|
24
|
+
jest,
|
|
25
|
+
} from '@jest/globals';
|
|
26
|
+
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
27
|
+
import * as assert from 'assert';
|
|
28
|
+
import { z, type Genkit } from 'genkit';
|
|
29
|
+
import { genkit, type GenkitBeta } from 'genkit/beta';
|
|
30
|
+
import { appendSpan } from 'genkit/tracing';
|
|
31
|
+
import {
|
|
32
|
+
__forceFlushSpansForTesting,
|
|
33
|
+
__getSpanExporterForTesting,
|
|
34
|
+
} from '../src/gcpOpenTelemetry.js';
|
|
35
|
+
import { enableGoogleCloudTelemetry } from '../src/index.js';
|
|
36
|
+
|
|
37
|
+
jest.mock('../src/auth.js', () => {
|
|
38
|
+
const original = jest.requireActual('../src/auth.js');
|
|
39
|
+
return {
|
|
40
|
+
...(original || {}),
|
|
41
|
+
resolveCurrentPrincipal: jest.fn().mockImplementation(() => {
|
|
42
|
+
return Promise.resolve({
|
|
43
|
+
projectId: 'test',
|
|
44
|
+
serviceAccountEmail: 'test@test.com',
|
|
45
|
+
});
|
|
46
|
+
}),
|
|
47
|
+
credentialsFromEnvironment: jest.fn().mockImplementation(() => {
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
projectId: 'test',
|
|
50
|
+
credentials: {
|
|
51
|
+
client_email: 'test@genkit.com',
|
|
52
|
+
private_key: '-----BEGIN PRIVATE KEY-----',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('GoogleCloudTracing', () => {
|
|
60
|
+
let ai: GenkitBeta;
|
|
61
|
+
|
|
62
|
+
beforeAll(async () => {
|
|
63
|
+
process.env.GCLOUD_PROJECT = 'test';
|
|
64
|
+
process.env.GENKIT_ENV = 'dev';
|
|
65
|
+
await enableGoogleCloudTelemetry({
|
|
66
|
+
projectId: 'test',
|
|
67
|
+
forceDevExport: false,
|
|
68
|
+
});
|
|
69
|
+
ai = genkit({});
|
|
70
|
+
});
|
|
71
|
+
beforeEach(async () => {
|
|
72
|
+
__getSpanExporterForTesting().reset();
|
|
73
|
+
});
|
|
74
|
+
afterAll(async () => {
|
|
75
|
+
await ai.stopServers();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('writes traces', async () => {
|
|
79
|
+
const testFlow = createFlow(ai, 'testFlow');
|
|
80
|
+
|
|
81
|
+
await testFlow();
|
|
82
|
+
|
|
83
|
+
const spans = await getExportedSpans();
|
|
84
|
+
assert.equal(spans.length, 1);
|
|
85
|
+
assert.equal(spans[0].name, 'testFlow');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('Adjusts attributes to support GCP trace filtering', async () => {
|
|
89
|
+
const testFlow = createFlow(ai, 'testFlow');
|
|
90
|
+
|
|
91
|
+
await testFlow();
|
|
92
|
+
|
|
93
|
+
const spans = await getExportedSpans();
|
|
94
|
+
// Check some common attributes
|
|
95
|
+
assert.equal(spans[0].attributes['genkit/name'], 'testFlow');
|
|
96
|
+
assert.equal(spans[0].attributes['genkit/type'], 'action');
|
|
97
|
+
assert.equal(spans[0].attributes['genkit/metadata/subtype'], 'flow');
|
|
98
|
+
// Ensure we have no attributes with ':' because these are awkward to use in
|
|
99
|
+
// Cloud Trace.
|
|
100
|
+
const spanAttrKeys = Object.entries(spans[0].attributes).map(([k, v]) => k);
|
|
101
|
+
for (const key in spanAttrKeys) {
|
|
102
|
+
assert.equal(key.indexOf(':'), -1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('sub actions are contained within flows', async () => {
|
|
107
|
+
const testFlow = createFlow(ai, 'testFlow', async () => {
|
|
108
|
+
return await ai.run('subAction', async () => {
|
|
109
|
+
return await ai.run('subAction2', async () => {
|
|
110
|
+
return 'done';
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await testFlow();
|
|
116
|
+
|
|
117
|
+
const spans = await getExportedSpans();
|
|
118
|
+
assert.equal(spans.length, 3);
|
|
119
|
+
assert.equal(spans[2].name, 'testFlow');
|
|
120
|
+
assert.equal(spans[2].parentSpanId, undefined);
|
|
121
|
+
assert.equal(spans[1].name, 'subAction');
|
|
122
|
+
assert.equal(spans[1].parentSpanId, spans[2].spanContext().spanId);
|
|
123
|
+
assert.equal(spans[0].name, 'subAction2');
|
|
124
|
+
assert.equal(spans[0].parentSpanId, spans[1].spanContext().spanId);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('different flows run independently', async () => {
|
|
128
|
+
const testFlow1 = createFlow(ai, 'testFlow1');
|
|
129
|
+
const testFlow2 = createFlow(ai, 'testFlow2');
|
|
130
|
+
|
|
131
|
+
await testFlow1();
|
|
132
|
+
await testFlow2();
|
|
133
|
+
|
|
134
|
+
const spans = await getExportedSpans();
|
|
135
|
+
assert.equal(spans.length, 2);
|
|
136
|
+
assert.equal(spans[0].parentSpanId, undefined);
|
|
137
|
+
assert.equal(spans[1].parentSpanId, undefined);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('labels failed spans', async () => {
|
|
141
|
+
const testFlow = createFlow(ai, 'badFlow', async () => {
|
|
142
|
+
return await ai.run('badStep', async () => {
|
|
143
|
+
throw new Error('oh no!');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
await assert.rejects(async () => {
|
|
147
|
+
await testFlow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const spans = await getExportedSpans();
|
|
151
|
+
|
|
152
|
+
expect(spans).toHaveLength(2);
|
|
153
|
+
|
|
154
|
+
expect(spans[0].name).toEqual('badStep');
|
|
155
|
+
expect(spans[0].attributes['genkit/failedSpan']).toEqual('badStep');
|
|
156
|
+
expect(spans[0].attributes['genkit/failedPath']).toEqual(
|
|
157
|
+
'/{badFlow,t:flow}/{badStep,t:flowStep}'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(spans[1].attributes['genkit/isRoot']).toEqual(true);
|
|
161
|
+
expect(spans[1].attributes['genkit/rootState']).toEqual('error');
|
|
162
|
+
expect(spans[1].attributes['genkit/state']).toEqual('error');
|
|
163
|
+
expect(spans[1].attributes['genkit/failedSpan']).toBeUndefined();
|
|
164
|
+
expect(spans[1].attributes['genkit/failedPath']).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('labels multiple failed spans', async () => {
|
|
168
|
+
const testFlow = createFlow(ai, 'badFlow', async () => {
|
|
169
|
+
await Promise.all([
|
|
170
|
+
ai.run('sub1', async () => {
|
|
171
|
+
throw new Error('oh no!');
|
|
172
|
+
}),
|
|
173
|
+
ai.run('sub2', async () => {
|
|
174
|
+
throw new Error('oh no!');
|
|
175
|
+
}),
|
|
176
|
+
]);
|
|
177
|
+
return 'root is ok';
|
|
178
|
+
});
|
|
179
|
+
await assert.rejects(async () => {
|
|
180
|
+
await testFlow();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const spans = await getExportedSpans();
|
|
184
|
+
|
|
185
|
+
expect(spans).toHaveLength(3);
|
|
186
|
+
|
|
187
|
+
const rootSpan = spans.find((s) => s.name === 'badFlow')!;
|
|
188
|
+
const sub1Span = spans.find((s) => s.name === 'sub1')!;
|
|
189
|
+
const sub2Span = spans.find((s) => s.name === 'sub2')!;
|
|
190
|
+
|
|
191
|
+
expect(rootSpan.attributes['genkit/failedSpan']).toBeUndefined();
|
|
192
|
+
expect(rootSpan.attributes['genkit/failedPath']).toBeUndefined();
|
|
193
|
+
|
|
194
|
+
expect(sub1Span.attributes['genkit/failedSpan']).toEqual('sub1');
|
|
195
|
+
expect(sub1Span.attributes['genkit/failedPath']).toEqual(
|
|
196
|
+
'/{badFlow,t:flow}/{sub1,t:flowStep}'
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(sub2Span.attributes['genkit/failedSpan']).toEqual('sub2');
|
|
200
|
+
expect(sub1Span.attributes['genkit/failedPath']).toEqual(
|
|
201
|
+
'/{badFlow,t:flow}/{sub1,t:flowStep}'
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('labels the root feature', async () => {
|
|
206
|
+
const testFlow = createFlow(ai, 'niceFlow', async () => {
|
|
207
|
+
return ai.run('niceStep', async () => {});
|
|
208
|
+
});
|
|
209
|
+
await testFlow();
|
|
210
|
+
|
|
211
|
+
const spans = await getExportedSpans();
|
|
212
|
+
assert.equal(spans[0].name, 'niceStep');
|
|
213
|
+
assert.equal(spans[0].attributes['genkit/feature'], undefined);
|
|
214
|
+
assert.equal(spans[1].name, 'niceFlow');
|
|
215
|
+
assert.equal(spans[1].attributes['genkit/feature'], 'niceFlow');
|
|
216
|
+
assert.equal(spans[1].attributes['genkit/rootState'], 'success');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('marks the root feature failed when it is the failure', async () => {
|
|
220
|
+
const testFlow = createFlow(ai, 'failingFlow', async () => {
|
|
221
|
+
await ai.run('good step', async () => {
|
|
222
|
+
return 'nothing going on here';
|
|
223
|
+
});
|
|
224
|
+
throw new Error('oops!');
|
|
225
|
+
});
|
|
226
|
+
await assert.rejects(async () => {
|
|
227
|
+
await testFlow();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const spans = await getExportedSpans();
|
|
231
|
+
|
|
232
|
+
assert.equal(spans.length, 2);
|
|
233
|
+
assert.equal(spans[0].attributes['genkit/state'], 'success');
|
|
234
|
+
assert.equal(spans[1].attributes['genkit/name'], 'failingFlow');
|
|
235
|
+
assert.equal(spans[1].attributes['genkit/failedSpan'], 'failingFlow');
|
|
236
|
+
assert.equal(
|
|
237
|
+
spans[1].attributes['genkit/failedPath'],
|
|
238
|
+
'/{failingFlow,t:flow}'
|
|
239
|
+
);
|
|
240
|
+
assert.equal(spans[1].attributes['genkit/isRoot'], true);
|
|
241
|
+
assert.equal(spans[1].attributes['genkit/rootState'], 'error');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('adds the genkit/model label for model actions', async () => {
|
|
245
|
+
const echoModel = ai.defineModel(
|
|
246
|
+
{
|
|
247
|
+
name: 'echoModel',
|
|
248
|
+
},
|
|
249
|
+
async (request) => {
|
|
250
|
+
return {
|
|
251
|
+
message: {
|
|
252
|
+
role: 'model',
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
text:
|
|
256
|
+
'Echo: ' +
|
|
257
|
+
request.messages
|
|
258
|
+
.map((m) => m.content.map((c) => c.text).join())
|
|
259
|
+
.join(),
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
finishReason: 'stop',
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
const testFlow = createFlow(ai, 'modelFlow', async () => {
|
|
268
|
+
return ai.run('runFlow', async () => {
|
|
269
|
+
await ai.generate({
|
|
270
|
+
model: echoModel,
|
|
271
|
+
prompt: 'Testing model telemetry',
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await testFlow();
|
|
277
|
+
|
|
278
|
+
const spans = await getExportedSpans();
|
|
279
|
+
|
|
280
|
+
assert.equal(spans[0].name, 'echoModel');
|
|
281
|
+
assert.equal(spans[0].attributes['genkit/model'], 'echoModel');
|
|
282
|
+
assert.equal(spans[1].name, 'generate');
|
|
283
|
+
assert.equal(spans[2].name, 'runFlow');
|
|
284
|
+
assert.equal(spans[3].name, 'modelFlow');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('attaches additional span', async () => {
|
|
288
|
+
await appendSpan(
|
|
289
|
+
'trace1',
|
|
290
|
+
'parent1',
|
|
291
|
+
{ name: 'span-name', metadata: { metadata_key: 'metadata_value' } },
|
|
292
|
+
{ ['label_key']: 'label_value' }
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const spans = await getExportedSpans();
|
|
296
|
+
const span = spans.find((it) => it.name === 'span-name');
|
|
297
|
+
assert.equal(Object.keys(span?.attributes || {}).length, 3);
|
|
298
|
+
assert.equal(span?.attributes['genkit/name'], 'span-name');
|
|
299
|
+
assert.equal(span?.attributes['label_key'], 'label_value');
|
|
300
|
+
assert.equal(
|
|
301
|
+
span?.attributes['genkit/metadata/metadata_key'],
|
|
302
|
+
'metadata_value'
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('writes sessionId and threadName for chats', async () => {
|
|
307
|
+
const testModel = ai.defineModel({ name: 'testModel' }, async () => {
|
|
308
|
+
return {
|
|
309
|
+
message: {
|
|
310
|
+
role: 'user',
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
text: 'response',
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
finishReason: 'stop',
|
|
318
|
+
usage: {
|
|
319
|
+
inputTokens: 10,
|
|
320
|
+
outputTokens: 14,
|
|
321
|
+
inputCharacters: 8,
|
|
322
|
+
outputCharacters: 16,
|
|
323
|
+
inputImages: 1,
|
|
324
|
+
outputImages: 3,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const chat = ai.chat();
|
|
330
|
+
|
|
331
|
+
await chat.send({ model: testModel, prompt: 'Sending test prompt' });
|
|
332
|
+
|
|
333
|
+
const spans = await getExportedSpans();
|
|
334
|
+
// We should get 3 spans from this chat -- "send" which delegates to "generate" which delegates to our "testModel"
|
|
335
|
+
// Only the top level span will have the sessionId and threadName until we make sessionId more ubiquitous
|
|
336
|
+
expect(spans).toHaveLength(3);
|
|
337
|
+
|
|
338
|
+
spans.forEach((span) => {
|
|
339
|
+
if (span.name === 'send') {
|
|
340
|
+
expect(span?.attributes['genkit/sessionId']).not.toBeUndefined();
|
|
341
|
+
expect(span?.attributes['genkit/threadName']).not.toBeUndefined();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Once we make the change to have sessionId on all relevant spans, then these should verify they are populated.
|
|
346
|
+
expect(span?.attributes['genkit/sessionId']).toBeUndefined();
|
|
347
|
+
expect(span?.attributes['genkit/threadName']).toBeUndefined();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
/** Helper to create a flow with no inputs or outputs */
|
|
352
|
+
function createFlow(
|
|
353
|
+
ai: Genkit,
|
|
354
|
+
name: string,
|
|
355
|
+
fn: () => Promise<any> = async () => {}
|
|
356
|
+
) {
|
|
357
|
+
return ai.defineFlow(
|
|
358
|
+
{
|
|
359
|
+
name,
|
|
360
|
+
inputSchema: z.void(),
|
|
361
|
+
outputSchema: z.void(),
|
|
362
|
+
},
|
|
363
|
+
fn
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Polls the in memory metric exporter until the genkit scope is found. */
|
|
368
|
+
async function getExportedSpans(maxAttempts = 200): Promise<ReadableSpan[]> {
|
|
369
|
+
__forceFlushSpansForTesting();
|
|
370
|
+
var attempts = 0;
|
|
371
|
+
while (attempts++ < maxAttempts) {
|
|
372
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
373
|
+
const found = __getSpanExporterForTesting().getFinishedSpans();
|
|
374
|
+
if (found.length > 0) {
|
|
375
|
+
return found;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
assert.fail(`Timed out while waiting for spans to be exported.`);
|
|
379
|
+
}
|
|
380
|
+
});
|