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.
Files changed (109) hide show
  1. package/package/LICENSE +203 -0
  2. package/package/README.md +83 -0
  3. package/package/lib/auth.d.mts +33 -0
  4. package/package/lib/auth.d.ts +33 -0
  5. package/package/lib/auth.js +70 -0
  6. package/package/lib/auth.js.map +1 -0
  7. package/package/lib/auth.mjs +45 -0
  8. package/package/lib/auth.mjs.map +1 -0
  9. package/package/lib/gcpLogger.d.mts +25 -0
  10. package/package/lib/gcpLogger.d.ts +25 -0
  11. package/package/lib/gcpLogger.js +118 -0
  12. package/package/lib/gcpLogger.js.map +1 -0
  13. package/package/lib/gcpLogger.mjs +82 -0
  14. package/package/lib/gcpLogger.mjs.map +1 -0
  15. package/package/lib/gcpOpenTelemetry.d.mts +59 -0
  16. package/package/lib/gcpOpenTelemetry.d.ts +59 -0
  17. package/package/lib/gcpOpenTelemetry.js +374 -0
  18. package/package/lib/gcpOpenTelemetry.js.map +1 -0
  19. package/package/lib/gcpOpenTelemetry.mjs +364 -0
  20. package/package/lib/gcpOpenTelemetry.mjs.map +1 -0
  21. package/package/lib/index.d.mts +36 -0
  22. package/package/lib/index.d.ts +36 -0
  23. package/package/lib/index.js +56 -0
  24. package/package/lib/index.js.map +1 -0
  25. package/package/lib/index.mjs +29 -0
  26. package/package/lib/index.mjs.map +1 -0
  27. package/package/lib/metrics.d.mts +65 -0
  28. package/package/lib/metrics.d.ts +65 -0
  29. package/package/lib/metrics.js +91 -0
  30. package/package/lib/metrics.js.map +1 -0
  31. package/package/lib/metrics.mjs +65 -0
  32. package/package/lib/metrics.mjs.map +1 -0
  33. package/package/lib/model-armor.d.mts +59 -0
  34. package/package/lib/model-armor.d.ts +59 -0
  35. package/package/lib/model-armor.js +205 -0
  36. package/package/lib/model-armor.js.map +1 -0
  37. package/package/lib/model-armor.mjs +181 -0
  38. package/package/lib/model-armor.mjs.map +1 -0
  39. package/package/lib/telemetry/action.d.mts +27 -0
  40. package/package/lib/telemetry/action.d.ts +27 -0
  41. package/package/lib/telemetry/action.js +92 -0
  42. package/package/lib/telemetry/action.js.map +1 -0
  43. package/package/lib/telemetry/action.mjs +73 -0
  44. package/package/lib/telemetry/action.mjs.map +1 -0
  45. package/package/lib/telemetry/defaults.d.mts +30 -0
  46. package/package/lib/telemetry/defaults.d.ts +30 -0
  47. package/package/lib/telemetry/defaults.js +70 -0
  48. package/package/lib/telemetry/defaults.js.map +1 -0
  49. package/package/lib/telemetry/defaults.mjs +46 -0
  50. package/package/lib/telemetry/defaults.mjs.map +1 -0
  51. package/package/lib/telemetry/engagement.d.mts +35 -0
  52. package/package/lib/telemetry/engagement.d.ts +35 -0
  53. package/package/lib/telemetry/engagement.js +106 -0
  54. package/package/lib/telemetry/engagement.js.map +1 -0
  55. package/package/lib/telemetry/engagement.mjs +85 -0
  56. package/package/lib/telemetry/engagement.mjs.map +1 -0
  57. package/package/lib/telemetry/feature.d.mts +35 -0
  58. package/package/lib/telemetry/feature.d.ts +35 -0
  59. package/package/lib/telemetry/feature.js +142 -0
  60. package/package/lib/telemetry/feature.js.map +1 -0
  61. package/package/lib/telemetry/feature.mjs +127 -0
  62. package/package/lib/telemetry/feature.mjs.map +1 -0
  63. package/package/lib/telemetry/generate.d.mts +53 -0
  64. package/package/lib/telemetry/generate.d.ts +53 -0
  65. package/package/lib/telemetry/generate.js +326 -0
  66. package/package/lib/telemetry/generate.js.map +1 -0
  67. package/package/lib/telemetry/generate.mjs +314 -0
  68. package/package/lib/telemetry/generate.mjs.map +1 -0
  69. package/package/lib/telemetry/path.d.mts +32 -0
  70. package/package/lib/telemetry/path.d.ts +32 -0
  71. package/package/lib/telemetry/path.js +91 -0
  72. package/package/lib/telemetry/path.js.map +1 -0
  73. package/package/lib/telemetry/path.mjs +78 -0
  74. package/package/lib/telemetry/path.mjs.map +1 -0
  75. package/package/lib/types.d.mts +121 -0
  76. package/package/lib/types.d.ts +121 -0
  77. package/package/lib/types.js +17 -0
  78. package/package/lib/types.js.map +1 -0
  79. package/package/lib/types.mjs +1 -0
  80. package/package/lib/types.mjs.map +1 -0
  81. package/package/lib/utils.d.mts +57 -0
  82. package/package/lib/utils.d.ts +57 -0
  83. package/package/lib/utils.js +143 -0
  84. package/package/lib/utils.js.map +1 -0
  85. package/package/lib/utils.mjs +104 -0
  86. package/package/lib/utils.mjs.map +1 -0
  87. package/package/package.json +90 -0
  88. package/package/src/auth.ts +89 -0
  89. package/package/src/gcpLogger.ts +124 -0
  90. package/package/src/gcpOpenTelemetry.ts +485 -0
  91. package/package/src/index.ts +59 -0
  92. package/package/src/metrics.ts +122 -0
  93. package/package/src/model-armor.ts +317 -0
  94. package/package/src/telemetry/action.ts +106 -0
  95. package/package/src/telemetry/defaults.ts +72 -0
  96. package/package/src/telemetry/engagement.ts +120 -0
  97. package/package/src/telemetry/feature.ts +170 -0
  98. package/package/src/telemetry/generate.ts +454 -0
  99. package/package/src/telemetry/path.ts +111 -0
  100. package/package/src/types.ts +133 -0
  101. package/package/src/utils.ts +175 -0
  102. package/package/tests/logs_no_input_output_test.ts +267 -0
  103. package/package/tests/logs_session_test.ts +219 -0
  104. package/package/tests/logs_test.ts +633 -0
  105. package/package/tests/metrics_test.ts +792 -0
  106. package/package/tests/model_armor_test.ts +336 -0
  107. package/package/tests/traces_test.ts +380 -0
  108. package/package/typedoc.json +3 -0
  109. package/package.json +10 -0
@@ -0,0 +1,317 @@
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 { ModelArmorClient, protos } from '@google-cloud/modelarmor';
18
+ import { GenkitError } from 'genkit';
19
+ import {
20
+ GenerateRequest,
21
+ GenerateResponseData,
22
+ MessageData,
23
+ ModelMiddleware,
24
+ Part,
25
+ } from 'genkit/model';
26
+ import { runInNewSpan } from 'genkit/tracing';
27
+
28
+ export interface ModelArmorOptions {
29
+ templateName: string;
30
+ client?: ModelArmorClient;
31
+ /**
32
+ * Options for the Model Armor client (e.g. apiEndpoint).
33
+ */
34
+ clientOptions?: ConstructorParameters<typeof ModelArmorClient>[0];
35
+ /**
36
+ * What to sanitize. Defaults to 'all'.
37
+ */
38
+ protectionTarget?: 'all' | 'userPrompt' | 'modelResponse';
39
+ /**
40
+ * Whether to block on SDP match even if the content was successfully de-identified.
41
+ * Defaults to false (lenient).
42
+ */
43
+ strictSdpEnforcement?: boolean;
44
+ /**
45
+ * List of filters to enforce. If not specified, all filters are enforced.
46
+ * Possible values: 'rai', 'pi_and_jailbreak', 'malicious_uris', 'csam', 'sdp'.
47
+ */
48
+ filters?: (
49
+ | 'rai'
50
+ | 'pi_and_jailbreak'
51
+ | 'malicious_uris'
52
+ | 'csam'
53
+ | 'sdp'
54
+ | (string & {})
55
+ )[];
56
+ /**
57
+ * Whether to apply the de-identification results to the content.
58
+ * - If true, the default logic (replace text, preserve structure) is used.
59
+ * - If false, no changes are applied.
60
+ * - If a function, it is called with the messages and SDP result, and should return the new messages.
61
+ *
62
+ * Defaults to false.
63
+ */
64
+ applyDeidentificationResults?:
65
+ | boolean
66
+ | ((data: {
67
+ messages: MessageData[];
68
+ sdpResult: protos.google.cloud.modelarmor.v1.ISdpFilterResult;
69
+ }) => MessageData[] | undefined);
70
+ }
71
+
72
+ function extractText(parts: Part[]): string {
73
+ return parts.map((p) => p.text || '').join('');
74
+ }
75
+
76
+ /**
77
+ * If SDP (Sensitive Data Protection) filter returns sanitized data,
78
+ * we swap out the data with sanitized data.
79
+ */
80
+ function applySdp(
81
+ messages: MessageData[],
82
+ targetIndex: number,
83
+ result: protos.google.cloud.modelarmor.v1.ISanitizationResult,
84
+ options: ModelArmorOptions
85
+ ): { sdpApplied: boolean; messages: MessageData[] } {
86
+ const sdpFilterResult = result.filterResults?.['sdp']?.sdpFilterResult;
87
+
88
+ if (!sdpFilterResult) {
89
+ return { sdpApplied: false, messages };
90
+ }
91
+
92
+ // If user provided applyDeidentificationResults, we use it to apply
93
+ // the deidentification results.
94
+ if (typeof options.applyDeidentificationResults === 'function') {
95
+ const newMessages = options.applyDeidentificationResults({
96
+ messages,
97
+ sdpResult: sdpFilterResult,
98
+ });
99
+ if (!newMessages) {
100
+ return { sdpApplied: false, messages };
101
+ }
102
+ const sdpApplied = !!sdpFilterResult.deidentifyResult?.data?.text;
103
+ return { sdpApplied, messages: newMessages };
104
+ }
105
+
106
+ // if applyDeidentificationResults is set to true, we use the default/basic
107
+ // approach to apply the results.
108
+ if (options.applyDeidentificationResults === true) {
109
+ const deidentifyResult = sdpFilterResult.deidentifyResult;
110
+ if (deidentifyResult && deidentifyResult.data?.text) {
111
+ const targetMessage = messages[targetIndex];
112
+ const nonTextParts = targetMessage.content.filter((p) => !p.text);
113
+ const newContent = [
114
+ ...nonTextParts,
115
+ { text: deidentifyResult.data.text },
116
+ ];
117
+ const newMessages = [...messages];
118
+ newMessages[targetIndex] = { ...targetMessage, content: newContent };
119
+ return {
120
+ sdpApplied: true,
121
+ messages: newMessages,
122
+ };
123
+ }
124
+ }
125
+
126
+ return { sdpApplied: false, messages };
127
+ }
128
+
129
+ function shouldBlock(
130
+ result: protos.google.cloud.modelarmor.v1.ISanitizationResult,
131
+ options: ModelArmorOptions,
132
+ sdpApplied: boolean
133
+ ): boolean {
134
+ if (result.filterMatchState !== 'MATCH_FOUND') {
135
+ return false;
136
+ }
137
+ // Check if we should block.
138
+ // If strict SDP enforcement is enabled and SDP was applied, we must block.
139
+ if (options.strictSdpEnforcement && sdpApplied) {
140
+ return true;
141
+ }
142
+ // Otherwise, check if any active filter matched.
143
+ if (result.filterResults) {
144
+ for (const [key, filterResult] of Object.entries(result.filterResults)) {
145
+ if (options.filters && !options.filters.includes(key)) continue;
146
+ if (key === 'sdp' && sdpApplied) continue;
147
+
148
+ // Look for matchState in the nested object
149
+ // e.g. filterResult.raiFilterResult.matchState
150
+ const nestedResult = Object.values(filterResult)[0];
151
+ if (nestedResult?.matchState === 'MATCH_FOUND') {
152
+ return true;
153
+ }
154
+ }
155
+ }
156
+ return false;
157
+ }
158
+
159
+ async function sanitizeUserPrompt(
160
+ req: GenerateRequest,
161
+ client: ModelArmorClient,
162
+ options: ModelArmorOptions
163
+ ) {
164
+ let targetMessageIndex = -1;
165
+ // Find the last user message to sanitize
166
+ for (let i = req.messages.length - 1; i >= 0; i--) {
167
+ if (req.messages[i].role === 'user') {
168
+ targetMessageIndex = i;
169
+ break;
170
+ }
171
+ }
172
+
173
+ if (targetMessageIndex !== -1) {
174
+ const userMessage = req.messages[targetMessageIndex];
175
+ const promptText = extractText(userMessage.content);
176
+
177
+ if (promptText) {
178
+ await runInNewSpan(
179
+ { metadata: { name: 'sanitizeUserPrompt' } },
180
+ async (meta) => {
181
+ meta.input = {
182
+ name: options.templateName,
183
+ userPromptData: {
184
+ text: promptText,
185
+ },
186
+ };
187
+ const [response] = await client.sanitizeUserPrompt({
188
+ name: options.templateName,
189
+ userPromptData: {
190
+ text: promptText,
191
+ },
192
+ });
193
+ meta.output = response;
194
+
195
+ if (response.sanitizationResult) {
196
+ const result = response.sanitizationResult;
197
+ const { sdpApplied, messages: modifiedMessages } = applySdp(
198
+ req.messages,
199
+ targetMessageIndex,
200
+ result,
201
+ options
202
+ );
203
+
204
+ if (
205
+ sdpApplied ||
206
+ typeof options.applyDeidentificationResults === 'function'
207
+ ) {
208
+ req.messages = modifiedMessages;
209
+ }
210
+
211
+ if (shouldBlock(result, options, sdpApplied)) {
212
+ throw new GenkitError({
213
+ status: 'PERMISSION_DENIED',
214
+ message: 'Model Armor blocked user prompt.',
215
+ detail: result,
216
+ });
217
+ }
218
+ }
219
+ }
220
+ );
221
+ }
222
+ }
223
+ }
224
+
225
+ async function sanitizeModelResponse(
226
+ response: GenerateResponseData,
227
+ client: ModelArmorClient,
228
+ options: ModelArmorOptions
229
+ ) {
230
+ const usingMessageProp = !!response.message;
231
+ const candidates = response.message
232
+ ? [{ index: 0, message: response.message, finishReason: 'stop' }]
233
+ : response.candidates || [];
234
+
235
+ for (const candidate of candidates) {
236
+ const modelText = extractText(candidate.message.content);
237
+
238
+ if (modelText) {
239
+ await runInNewSpan(
240
+ { metadata: { name: 'sanitizeModelResponse' } },
241
+ async (meta) => {
242
+ meta.input = {
243
+ name: options.templateName,
244
+ modelResponseData: {
245
+ text: modelText,
246
+ },
247
+ };
248
+ const [apiResponse] = await client.sanitizeModelResponse({
249
+ name: options.templateName,
250
+ modelResponseData: {
251
+ text: modelText,
252
+ },
253
+ });
254
+ meta.output = apiResponse;
255
+
256
+ if (apiResponse.sanitizationResult) {
257
+ const result = apiResponse.sanitizationResult;
258
+ const { sdpApplied, messages: modifiedMessages } = applySdp(
259
+ [candidate.message],
260
+ 0,
261
+ result,
262
+ options
263
+ );
264
+
265
+ if (
266
+ sdpApplied ||
267
+ typeof options.applyDeidentificationResults === 'function'
268
+ ) {
269
+ candidate.message = modifiedMessages[0];
270
+ }
271
+
272
+ if (shouldBlock(result, options, sdpApplied)) {
273
+ throw new GenkitError({
274
+ status: 'PERMISSION_DENIED',
275
+ message: 'Model Armor blocked model response.',
276
+ detail: result,
277
+ });
278
+ }
279
+ }
280
+ }
281
+ );
282
+ }
283
+ }
284
+
285
+ if (usingMessageProp && candidates.length > 0) {
286
+ response.message = candidates[0].message;
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Model Middleware that uses Google Cloud Model Armor to sanitize user prompts and model responses.
292
+ */
293
+ export function modelArmor(options: ModelArmorOptions): ModelMiddleware {
294
+ const client = options.client || new ModelArmorClient(options.clientOptions);
295
+ const protectionTarget = options.protectionTarget ?? 'all';
296
+ const protectUserPrompt =
297
+ protectionTarget === 'all' || protectionTarget === 'userPrompt';
298
+ const protectModelResponse =
299
+ protectionTarget === 'all' || protectionTarget === 'modelResponse';
300
+
301
+ return async (req, next) => {
302
+ // 1. Sanitize User Prompt
303
+ if (protectUserPrompt) {
304
+ await sanitizeUserPrompt(req, client, options);
305
+ }
306
+
307
+ // 2. Call Model
308
+ const response = await next(req);
309
+
310
+ // 3. Sanitize Model Response
311
+ if (protectModelResponse) {
312
+ await sanitizeModelResponse(response, client, options);
313
+ }
314
+
315
+ return response;
316
+ };
317
+ }
@@ -0,0 +1,106 @@
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 type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
18
+ import { logger } from 'genkit/logging';
19
+ import { toDisplayPath } from 'genkit/tracing';
20
+ import { type Telemetry } from '../metrics.js';
21
+ import {
22
+ createCommonLogAttributes,
23
+ extractOuterFeatureNameFromPath,
24
+ truncate,
25
+ truncatePath,
26
+ } from '../utils.js';
27
+
28
+ class ActionTelemetry implements Telemetry {
29
+ tick(
30
+ span: ReadableSpan,
31
+ logInputAndOutput: boolean,
32
+ projectId?: string
33
+ ): void {
34
+ if (!logInputAndOutput) {
35
+ return;
36
+ }
37
+ const attributes = span.attributes;
38
+ const actionName = (attributes['genkit:name'] as string) || '<unknown>';
39
+ const subtype = attributes['genkit:metadata:subtype'] as string;
40
+
41
+ if (subtype === 'tool' || actionName === 'generate') {
42
+ const path = (attributes['genkit:path'] as string) || '<unknown>';
43
+ const input = truncate(attributes['genkit:input'] as string);
44
+ const output = truncate(attributes['genkit:output'] as string);
45
+ const sessionId = attributes['genkit:sessionId'] as string;
46
+ const threadName = attributes['genkit:threadName'] as string;
47
+ let featureName = extractOuterFeatureNameFromPath(path);
48
+ if (!featureName || featureName === '<unknown>') {
49
+ featureName = actionName;
50
+ }
51
+
52
+ if (input) {
53
+ this.writeLog(
54
+ span,
55
+ 'Input',
56
+ featureName,
57
+ path,
58
+ input,
59
+ projectId,
60
+ sessionId,
61
+ threadName
62
+ );
63
+ }
64
+ if (output) {
65
+ this.writeLog(
66
+ span,
67
+ 'Output',
68
+ featureName,
69
+ path,
70
+ output,
71
+ projectId,
72
+ sessionId,
73
+ threadName
74
+ );
75
+ }
76
+ }
77
+ }
78
+
79
+ private writeLog(
80
+ span: ReadableSpan,
81
+ tag: string,
82
+ featureName: string,
83
+ qualifiedPath: string,
84
+ content: string,
85
+ projectId?: string,
86
+ sessionId?: string,
87
+ threadName?: string
88
+ ) {
89
+ const path = truncatePath(toDisplayPath(qualifiedPath));
90
+ const sharedMetadata = {
91
+ ...createCommonLogAttributes(span, projectId),
92
+ path,
93
+ qualifiedPath,
94
+ featureName,
95
+ sessionId,
96
+ threadName,
97
+ };
98
+ logger.logStructured(`${tag}[${path}, ${featureName}]`, {
99
+ ...sharedMetadata,
100
+ content,
101
+ });
102
+ }
103
+ }
104
+
105
+ const actionTelemetry = new ActionTelemetry();
106
+ export { actionTelemetry };
@@ -0,0 +1,72 @@
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 { AlwaysOnSampler } from '@opentelemetry/sdk-trace-base';
18
+ import { isDevEnv } from 'genkit';
19
+ import type {
20
+ GcpTelemetryConfig,
21
+ GcpTelemetryConfigOptions,
22
+ } from '../types.js';
23
+
24
+ /** Consolidated defaults for telemetry configuration. */
25
+
26
+ export const TelemetryConfigs = {
27
+ defaults: (overrides: GcpTelemetryConfigOptions = {}): GcpTelemetryConfig => {
28
+ return isDevEnv()
29
+ ? TelemetryConfigs.developmentDefaults(overrides)
30
+ : TelemetryConfigs.productionDefaults(overrides);
31
+ },
32
+
33
+ developmentDefaults: (
34
+ overrides: GcpTelemetryConfigOptions = {}
35
+ ): GcpTelemetryConfig => {
36
+ const defaults = {
37
+ sampler: new AlwaysOnSampler(),
38
+ autoInstrumentation: true,
39
+ autoInstrumentationConfig: {
40
+ '@opentelemetry/instrumentation-dns': { enabled: false },
41
+ },
42
+ instrumentations: [],
43
+ metricExportIntervalMillis: 5_000,
44
+ metricExportTimeoutMillis: 5_000,
45
+ disableMetrics: false,
46
+ disableTraces: false,
47
+ exportInputAndOutput: !overrides.disableLoggingInputAndOutput,
48
+ export: !!overrides.forceDevExport, // false
49
+ };
50
+ return { ...defaults, ...overrides };
51
+ },
52
+
53
+ productionDefaults: (
54
+ overrides: GcpTelemetryConfigOptions = {}
55
+ ): GcpTelemetryConfig => {
56
+ const defaults = {
57
+ sampler: new AlwaysOnSampler(),
58
+ autoInstrumentation: true,
59
+ autoInstrumentationConfig: {
60
+ '@opentelemetry/instrumentation-dns': { enabled: false },
61
+ },
62
+ instrumentations: [],
63
+ metricExportIntervalMillis: 300_000,
64
+ metricExportTimeoutMillis: 300_000,
65
+ disableMetrics: false,
66
+ disableTraces: false,
67
+ exportInputAndOutput: !overrides.disableLoggingInputAndOutput,
68
+ export: true,
69
+ };
70
+ return { ...defaults, ...overrides };
71
+ },
72
+ };
@@ -0,0 +1,120 @@
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 { ValueType, type Attributes } from '@opentelemetry/api';
18
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
19
+ import { GENKIT_VERSION } from 'genkit';
20
+ import { logger } from 'genkit/logging';
21
+ import {
22
+ MetricCounter,
23
+ internalMetricNamespaceWrap,
24
+ type Telemetry,
25
+ } from '../metrics.js';
26
+ import { createCommonLogAttributes, truncate } from '../utils.js';
27
+
28
+ class EngagementTelemetry implements Telemetry {
29
+ /**
30
+ * Wraps the declared metrics in a Genkit-specific, internal namespace.
31
+ */
32
+ private _N = internalMetricNamespaceWrap.bind(null, 'engagement');
33
+
34
+ private feedbackCounter = new MetricCounter(this._N('feedback'), {
35
+ description: 'Counts calls to genkit flows.',
36
+ valueType: ValueType.INT,
37
+ });
38
+
39
+ private acceptanceCounter = new MetricCounter(this._N('acceptance'), {
40
+ description: 'Tracks unique flow paths per flow.',
41
+ valueType: ValueType.INT,
42
+ });
43
+
44
+ tick(
45
+ span: ReadableSpan,
46
+ logInputAndOutput: boolean,
47
+ projectId?: string
48
+ ): void {
49
+ const subtype = span.attributes['genkit:metadata:subtype'] as string;
50
+
51
+ if (subtype === 'userFeedback') {
52
+ this.writeUserFeedback(span, projectId);
53
+ return;
54
+ }
55
+
56
+ if (subtype === 'userAcceptance') {
57
+ this.writeUserAcceptance(span, projectId);
58
+ return;
59
+ }
60
+
61
+ logger.warn(`Unknown user engagement subtype: ${subtype}`);
62
+ }
63
+
64
+ private writeUserFeedback(span: ReadableSpan, projectId?: string) {
65
+ const attributes = span.attributes;
66
+ const name = this.extractTraceName(attributes);
67
+
68
+ const dimensions = {
69
+ name,
70
+ value: attributes['genkit:metadata:feedbackValue'],
71
+ hasText: !!attributes['genkit:metadata:textFeedback'],
72
+ source: 'ts',
73
+ sourceVersion: GENKIT_VERSION,
74
+ };
75
+ this.feedbackCounter.add(1, dimensions);
76
+
77
+ const metadata = {
78
+ ...createCommonLogAttributes(span, projectId),
79
+ feedbackValue: attributes['genkit:metadata:feedbackValue'],
80
+ };
81
+ if (attributes['genkit:metadata:textFeedback']) {
82
+ metadata['textFeedback'] = truncate(
83
+ attributes['genkit:metadata:textFeedback'] as string
84
+ );
85
+ }
86
+ logger.logStructured(`UserFeedback[${name}]`, metadata);
87
+ }
88
+
89
+ private writeUserAcceptance(span: ReadableSpan, projectId?: string) {
90
+ const attributes = span.attributes;
91
+ const name = this.extractTraceName(attributes);
92
+
93
+ const dimensions = {
94
+ name,
95
+ value: attributes['genkit:metadata:acceptanceValue'],
96
+ source: 'ts',
97
+ sourceVersion: GENKIT_VERSION,
98
+ };
99
+ this.acceptanceCounter.add(1, dimensions);
100
+
101
+ const metadata = {
102
+ ...createCommonLogAttributes(span, projectId),
103
+ acceptanceValue: attributes['genkit:metadata:acceptanceValue'],
104
+ };
105
+ logger.logStructured(`UserAcceptance[${name}]`, metadata);
106
+ }
107
+
108
+ private extractTraceName(attributes: Attributes) {
109
+ const path = attributes['genkit:path'] as string;
110
+ if (!path || path === '<unknown>') {
111
+ return '<unknown>';
112
+ }
113
+
114
+ const name = path.match('/{(.+)}+');
115
+ return name ? name[1] : '<unknown>';
116
+ }
117
+ }
118
+
119
+ const engagementTelemetry = new EngagementTelemetry();
120
+ export { engagementTelemetry };