@sentry/core 10.30.0 → 10.32.0-alpha.0

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 (194) hide show
  1. package/build/cjs/attributes.js +98 -0
  2. package/build/cjs/attributes.js.map +1 -0
  3. package/build/cjs/client.js +11 -6
  4. package/build/cjs/client.js.map +1 -1
  5. package/build/cjs/envelope.js +41 -7
  6. package/build/cjs/envelope.js.map +1 -1
  7. package/build/cjs/index.js +30 -0
  8. package/build/cjs/index.js.map +1 -1
  9. package/build/cjs/integrations/eventFilters.js +1 -1
  10. package/build/cjs/integrations/eventFilters.js.map +1 -1
  11. package/build/cjs/integrations/mcp-server/correlation.js +17 -3
  12. package/build/cjs/integrations/mcp-server/correlation.js.map +1 -1
  13. package/build/cjs/integrations/mcp-server/sessionExtraction.js +44 -0
  14. package/build/cjs/integrations/mcp-server/sessionExtraction.js.map +1 -1
  15. package/build/cjs/integrations/mcp-server/transport.js +17 -3
  16. package/build/cjs/integrations/mcp-server/transport.js.map +1 -1
  17. package/build/cjs/integrations/requestdata.js +72 -6
  18. package/build/cjs/integrations/requestdata.js.map +1 -1
  19. package/build/cjs/integrations/serverSpanStreaming.js +51 -0
  20. package/build/cjs/integrations/serverSpanStreaming.js.map +1 -0
  21. package/build/cjs/semanticAttributes.js +36 -0
  22. package/build/cjs/semanticAttributes.js.map +1 -1
  23. package/build/cjs/spans/captureSpan.js +107 -0
  24. package/build/cjs/spans/captureSpan.js.map +1 -0
  25. package/build/cjs/spans/spanBuffer.js +117 -0
  26. package/build/cjs/spans/spanBuffer.js.map +1 -0
  27. package/build/cjs/spans/spanFirstUtils.js +187 -0
  28. package/build/cjs/spans/spanFirstUtils.js.map +1 -0
  29. package/build/cjs/tracing/ai/messageTruncation.js +120 -11
  30. package/build/cjs/tracing/ai/messageTruncation.js.map +1 -1
  31. package/build/cjs/tracing/google-genai/index.js +31 -14
  32. package/build/cjs/tracing/google-genai/index.js.map +1 -1
  33. package/build/cjs/tracing/google-genai/utils.js +24 -5
  34. package/build/cjs/tracing/google-genai/utils.js.map +1 -1
  35. package/build/cjs/tracing/sentrySpan.js +31 -0
  36. package/build/cjs/tracing/sentrySpan.js.map +1 -1
  37. package/build/cjs/tracing/trace.js +1 -0
  38. package/build/cjs/tracing/trace.js.map +1 -1
  39. package/build/cjs/tracing/vercel-ai/index.js +4 -5
  40. package/build/cjs/tracing/vercel-ai/index.js.map +1 -1
  41. package/build/cjs/tracing/vercel-ai/vercel-ai-attributes.js +0 -10
  42. package/build/cjs/tracing/vercel-ai/vercel-ai-attributes.js.map +1 -1
  43. package/build/cjs/utils/applyScopeDataToEvent.js +5 -0
  44. package/build/cjs/utils/applyScopeDataToEvent.js.map +1 -1
  45. package/build/cjs/utils/beforeSendSpan.js +36 -0
  46. package/build/cjs/utils/beforeSendSpan.js.map +1 -0
  47. package/build/cjs/utils/featureFlags.js +1 -0
  48. package/build/cjs/utils/featureFlags.js.map +1 -1
  49. package/build/cjs/utils/request.js +75 -12
  50. package/build/cjs/utils/request.js.map +1 -1
  51. package/build/cjs/utils/should-ignore-span.js +31 -9
  52. package/build/cjs/utils/should-ignore-span.js.map +1 -1
  53. package/build/cjs/utils/spanUtils.js +101 -2
  54. package/build/cjs/utils/spanUtils.js.map +1 -1
  55. package/build/cjs/utils/traceData.js +1 -4
  56. package/build/cjs/utils/traceData.js.map +1 -1
  57. package/build/cjs/utils/version.js +1 -1
  58. package/build/cjs/utils/version.js.map +1 -1
  59. package/build/esm/attributes.js +95 -0
  60. package/build/esm/attributes.js.map +1 -0
  61. package/build/esm/client.js +6 -1
  62. package/build/esm/client.js.map +1 -1
  63. package/build/esm/envelope.js +41 -8
  64. package/build/esm/envelope.js.map +1 -1
  65. package/build/esm/index.js +10 -4
  66. package/build/esm/index.js.map +1 -1
  67. package/build/esm/integrations/eventFilters.js +1 -1
  68. package/build/esm/integrations/eventFilters.js.map +1 -1
  69. package/build/esm/integrations/mcp-server/correlation.js +17 -3
  70. package/build/esm/integrations/mcp-server/correlation.js.map +1 -1
  71. package/build/esm/integrations/mcp-server/sessionExtraction.js +43 -1
  72. package/build/esm/integrations/mcp-server/sessionExtraction.js.map +1 -1
  73. package/build/esm/integrations/mcp-server/transport.js +18 -4
  74. package/build/esm/integrations/mcp-server/transport.js.map +1 -1
  75. package/build/esm/integrations/requestdata.js +72 -6
  76. package/build/esm/integrations/requestdata.js.map +1 -1
  77. package/build/esm/integrations/serverSpanStreaming.js +49 -0
  78. package/build/esm/integrations/serverSpanStreaming.js.map +1 -0
  79. package/build/esm/package.json +1 -1
  80. package/build/esm/semanticAttributes.js +26 -1
  81. package/build/esm/semanticAttributes.js.map +1 -1
  82. package/build/esm/spans/captureSpan.js +105 -0
  83. package/build/esm/spans/captureSpan.js.map +1 -0
  84. package/build/esm/spans/spanBuffer.js +115 -0
  85. package/build/esm/spans/spanBuffer.js.map +1 -0
  86. package/build/esm/spans/spanFirstUtils.js +183 -0
  87. package/build/esm/spans/spanFirstUtils.js.map +1 -0
  88. package/build/esm/tracing/ai/messageTruncation.js +121 -11
  89. package/build/esm/tracing/ai/messageTruncation.js.map +1 -1
  90. package/build/esm/tracing/google-genai/index.js +34 -17
  91. package/build/esm/tracing/google-genai/index.js.map +1 -1
  92. package/build/esm/tracing/google-genai/utils.js +24 -6
  93. package/build/esm/tracing/google-genai/utils.js.map +1 -1
  94. package/build/esm/tracing/sentrySpan.js +32 -1
  95. package/build/esm/tracing/sentrySpan.js.map +1 -1
  96. package/build/esm/tracing/trace.js +1 -0
  97. package/build/esm/tracing/trace.js.map +1 -1
  98. package/build/esm/tracing/vercel-ai/index.js +5 -6
  99. package/build/esm/tracing/vercel-ai/index.js.map +1 -1
  100. package/build/esm/tracing/vercel-ai/vercel-ai-attributes.js +1 -10
  101. package/build/esm/tracing/vercel-ai/vercel-ai-attributes.js.map +1 -1
  102. package/build/esm/utils/applyScopeDataToEvent.js +5 -0
  103. package/build/esm/utils/applyScopeDataToEvent.js.map +1 -1
  104. package/build/esm/utils/beforeSendSpan.js +33 -0
  105. package/build/esm/utils/beforeSendSpan.js.map +1 -0
  106. package/build/esm/utils/featureFlags.js +1 -0
  107. package/build/esm/utils/featureFlags.js.map +1 -1
  108. package/build/esm/utils/request.js +76 -13
  109. package/build/esm/utils/request.js.map +1 -1
  110. package/build/esm/utils/should-ignore-span.js +31 -9
  111. package/build/esm/utils/should-ignore-span.js.map +1 -1
  112. package/build/esm/utils/spanUtils.js +97 -3
  113. package/build/esm/utils/spanUtils.js.map +1 -1
  114. package/build/esm/utils/traceData.js +1 -4
  115. package/build/esm/utils/traceData.js.map +1 -1
  116. package/build/esm/utils/version.js +1 -1
  117. package/build/esm/utils/version.js.map +1 -1
  118. package/build/types/attributes.d.ts +2 -2
  119. package/build/types/attributes.d.ts.map +1 -1
  120. package/build/types/client.d.ts +40 -2
  121. package/build/types/client.d.ts.map +1 -1
  122. package/build/types/envelope.d.ts +6 -1
  123. package/build/types/envelope.d.ts.map +1 -1
  124. package/build/types/index.d.ts +12 -5
  125. package/build/types/index.d.ts.map +1 -1
  126. package/build/types/integrations/mcp-server/correlation.d.ts +2 -2
  127. package/build/types/integrations/mcp-server/correlation.d.ts.map +1 -1
  128. package/build/types/integrations/mcp-server/sessionExtraction.d.ts +13 -1
  129. package/build/types/integrations/mcp-server/sessionExtraction.d.ts.map +1 -1
  130. package/build/types/integrations/mcp-server/transport.d.ts.map +1 -1
  131. package/build/types/integrations/requestdata.d.ts.map +1 -1
  132. package/build/types/integrations/serverSpanStreaming.d.ts +8 -0
  133. package/build/types/integrations/serverSpanStreaming.d.ts.map +1 -0
  134. package/build/types/semanticAttributes.d.ts +21 -0
  135. package/build/types/semanticAttributes.d.ts.map +1 -1
  136. package/build/types/spans/captureSpan.d.ts +10 -0
  137. package/build/types/spans/captureSpan.d.ts.map +1 -0
  138. package/build/types/spans/spanBuffer.d.ts +31 -0
  139. package/build/types/spans/spanBuffer.d.ts.map +1 -0
  140. package/build/types/spans/spanFirstUtils.d.ts +20 -0
  141. package/build/types/spans/spanFirstUtils.d.ts.map +1 -0
  142. package/build/types/tracing/ai/messageTruncation.d.ts +0 -20
  143. package/build/types/tracing/ai/messageTruncation.d.ts.map +1 -1
  144. package/build/types/tracing/google-genai/index.d.ts.map +1 -1
  145. package/build/types/tracing/google-genai/utils.d.ts +25 -0
  146. package/build/types/tracing/google-genai/utils.d.ts.map +1 -1
  147. package/build/types/tracing/sentrySpan.d.ts +10 -1
  148. package/build/types/tracing/sentrySpan.d.ts.map +1 -1
  149. package/build/types/tracing/vercel-ai/index.d.ts.map +1 -1
  150. package/build/types/types-hoist/attributes.d.ts +19 -0
  151. package/build/types/types-hoist/attributes.d.ts.map +1 -0
  152. package/build/types/types-hoist/envelope.d.ts +22 -2
  153. package/build/types/types-hoist/envelope.d.ts.map +1 -1
  154. package/build/types/types-hoist/link.d.ts +2 -2
  155. package/build/types/types-hoist/link.d.ts.map +1 -1
  156. package/build/types/types-hoist/options.d.ts +31 -2
  157. package/build/types/types-hoist/options.d.ts.map +1 -1
  158. package/build/types/types-hoist/span.d.ts +27 -0
  159. package/build/types/types-hoist/span.d.ts.map +1 -1
  160. package/build/types/utils/applyScopeDataToEvent.d.ts.map +1 -1
  161. package/build/types/utils/beforeSendSpan.d.ts +22 -0
  162. package/build/types/utils/beforeSendSpan.d.ts.map +1 -0
  163. package/build/types/utils/featureFlags.d.ts.map +1 -1
  164. package/build/types/utils/request.d.ts +1 -1
  165. package/build/types/utils/request.d.ts.map +1 -1
  166. package/build/types/utils/should-ignore-span.d.ts +3 -3
  167. package/build/types/utils/should-ignore-span.d.ts.map +1 -1
  168. package/build/types/utils/spanUtils.d.ts +26 -2
  169. package/build/types/utils/spanUtils.d.ts.map +1 -1
  170. package/build/types/utils/traceData.d.ts.map +1 -1
  171. package/build/types-ts3.8/attributes.d.ts +2 -2
  172. package/build/types-ts3.8/client.d.ts +40 -2
  173. package/build/types-ts3.8/envelope.d.ts +6 -1
  174. package/build/types-ts3.8/index.d.ts +12 -5
  175. package/build/types-ts3.8/integrations/mcp-server/correlation.d.ts +2 -2
  176. package/build/types-ts3.8/integrations/mcp-server/sessionExtraction.d.ts +13 -1
  177. package/build/types-ts3.8/integrations/serverSpanStreaming.d.ts +8 -0
  178. package/build/types-ts3.8/semanticAttributes.d.ts +21 -0
  179. package/build/types-ts3.8/spans/captureSpan.d.ts +10 -0
  180. package/build/types-ts3.8/spans/spanBuffer.d.ts +31 -0
  181. package/build/types-ts3.8/spans/spanFirstUtils.d.ts +20 -0
  182. package/build/types-ts3.8/tracing/ai/messageTruncation.d.ts +0 -20
  183. package/build/types-ts3.8/tracing/google-genai/utils.d.ts +25 -0
  184. package/build/types-ts3.8/tracing/sentrySpan.d.ts +10 -1
  185. package/build/types-ts3.8/types-hoist/attributes.d.ts +19 -0
  186. package/build/types-ts3.8/types-hoist/envelope.d.ts +22 -2
  187. package/build/types-ts3.8/types-hoist/link.d.ts +2 -2
  188. package/build/types-ts3.8/types-hoist/options.d.ts +31 -2
  189. package/build/types-ts3.8/types-hoist/span.d.ts +27 -0
  190. package/build/types-ts3.8/utils/beforeSendSpan.d.ts +22 -0
  191. package/build/types-ts3.8/utils/request.d.ts +1 -1
  192. package/build/types-ts3.8/utils/should-ignore-span.d.ts +3 -3
  193. package/build/types-ts3.8/utils/spanUtils.d.ts +26 -2
  194. package/package.json +1 -1
@@ -0,0 +1,183 @@
1
+ import { isAttributeObject, attributeValueToTypedAttributeValue } from '../attributes.js';
2
+ import { isPrimitive } from '../utils/is.js';
3
+ import { showSpanDropWarning } from '../utils/spanUtils.js';
4
+
5
+ /**
6
+ * Only set a span JSON attribute if it is not already set.
7
+ * This is used to safely set attributes on JSON objects without mutating already-ended span instances.
8
+ */
9
+ function safeSetSpanJSONAttributes(
10
+ spanJSON,
11
+ newAttributes,
12
+ ) {
13
+ if (!spanJSON.attributes) {
14
+ spanJSON.attributes = {};
15
+ }
16
+
17
+ const originalAttributes = spanJSON.attributes;
18
+
19
+ Object.keys(newAttributes).forEach(key => {
20
+ if (!originalAttributes?.[key]) {
21
+ setAttributeOnSpanJSONWithMaybeUnit(
22
+ // type-casting here because we ensured above that the attributes object exists
23
+ spanJSON ,
24
+ key,
25
+ newAttributes[key],
26
+ );
27
+ }
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Apply a user-provided beforeSendSpan callback to a span JSON.
33
+ */
34
+ function applyBeforeSendSpanCallback(
35
+ span,
36
+ beforeSendSpan,
37
+ ) {
38
+ const modifedSpan = beforeSendSpan(span);
39
+ if (!modifedSpan) {
40
+ showSpanDropWarning();
41
+ return span;
42
+ }
43
+ return modifedSpan;
44
+ }
45
+
46
+ function setAttributeOnSpanJSONWithMaybeUnit(
47
+ spanJSON,
48
+ attributeKey,
49
+ attributeValue,
50
+ ) {
51
+ if (isAttributeObject(attributeValue)) {
52
+ const { value, unit } = attributeValue;
53
+
54
+ if (isSupportedSerializableType(value)) {
55
+ spanJSON.attributes[attributeKey] = attributeValueToTypedAttributeValue(attributeValue);
56
+ if (unit) {
57
+ spanJSON.attributes[attributeKey].unit = unit;
58
+ }
59
+ }
60
+ } else if (isSupportedSerializableType(attributeValue)) {
61
+ spanJSON.attributes[attributeKey] = attributeValueToTypedAttributeValue(attributeValue);
62
+ }
63
+ }
64
+
65
+ function isSupportedSerializableType(value) {
66
+ return ['string', 'number', 'boolean'].includes(typeof value) || Array.isArray(value);
67
+ }
68
+
69
+ // map of attributes->context keys for those attributes that don't correspond 1:1 to the context key
70
+ const explicitAttributeToContextMapping = {
71
+ 'os.build_id': 'os.build',
72
+ 'app.name': 'app.app_name',
73
+ 'app.identifier': 'app.app_identifier',
74
+ 'app.version': 'app.app_version',
75
+ 'app.memory': 'app.app_memory',
76
+ 'app.start_time': 'app.app_start_time',
77
+ };
78
+
79
+ const knownContexts = [
80
+ // set by `nodeContextIntegration`
81
+ 'app',
82
+ 'os',
83
+ 'device',
84
+ 'culture',
85
+ 'cloud_resource',
86
+ 'runtime',
87
+
88
+ // TODO: These need more thorough checking if they're all setting expected attributes
89
+
90
+ // set by the `instrumentPostgresJs`
91
+ 'postgresjsConnection',
92
+ // set by `ensureIsWrapped`
93
+ 'missing_instrumentation',
94
+ // set by `nodeProfilingIntegration`
95
+ 'profile',
96
+ // set by angular `init`
97
+ 'angular',
98
+ // set by AWS Lambda SDK
99
+ 'aws.lambda',
100
+ 'aws.cloudwatch.logs',
101
+ // set by `instrumentBunServe`
102
+ 'response',
103
+ // set by `trpcMiddleware`
104
+ 'trpc',
105
+ // set by `instrumentSupabaseClient`
106
+ 'supabase',
107
+ // set by `gcp.function.context`
108
+ 'gcp.function.context',
109
+ // set by nextjs SDK
110
+ 'nextjs',
111
+ // set by react SDK `captureReactException`, `init`
112
+ 'react',
113
+ // set by react SDK `createReduxEnhancer`
114
+ 'state',
115
+ // set by `replayIntegration`
116
+ 'Replays',
117
+ // Other information:
118
+ // - no need to handler feature flags `flags` context because it's already added to the active span
119
+ ];
120
+
121
+ /**
122
+ * Converts a context object to a set of attributes.
123
+ * Only includes attributes that are primitives (for now).
124
+ * @param contexts - The context object to convert.
125
+ * @returns The attributes object.
126
+ */
127
+ function contextsToAttributes(contexts) {
128
+ function contextToAttribute(context) {
129
+ return Object.keys(context).reduce(
130
+ (acc, key) => {
131
+ if (!isPrimitive(context[key])) {
132
+ return acc;
133
+ }
134
+ acc[key] = context[key];
135
+ return acc;
136
+ },
137
+ {} ,
138
+ );
139
+ }
140
+
141
+ const contextsWithPrimitiveValues = Object.keys(contexts).reduce((acc, key) => {
142
+ if (!knownContexts.includes(key)) {
143
+ return acc;
144
+ }
145
+ const context = contexts[key];
146
+ if (context) {
147
+ acc[key] = contextToAttribute(context);
148
+ }
149
+ return acc;
150
+ }, {} );
151
+
152
+ const explicitlyMappedAttributes = Object.entries(explicitAttributeToContextMapping).reduce(
153
+ (acc, [attributeKey, contextKey]) => {
154
+ const [contextName, contextValueKey] = contextKey.split('.');
155
+ if (contextName && contextValueKey && contextsWithPrimitiveValues[contextName]?.[contextValueKey]) {
156
+ acc[attributeKey] = contextsWithPrimitiveValues[contextName]?.[contextValueKey];
157
+ // now we delete this key from `contextsWithPrimitiveValues` so we don't include it in the next step
158
+ delete contextsWithPrimitiveValues[contextName]?.[contextValueKey];
159
+ }
160
+ return acc;
161
+ },
162
+ {} ,
163
+ );
164
+
165
+ return {
166
+ ...explicitlyMappedAttributes,
167
+ ...Object.entries(contextsWithPrimitiveValues).reduce(
168
+ (acc, [contextName, contextObj]) => {
169
+ contextObj &&
170
+ Object.entries(contextObj).forEach(([key, value]) => {
171
+ if (value) {
172
+ acc[`${contextName}.${key}`] = value;
173
+ }
174
+ });
175
+ return acc;
176
+ },
177
+ {} ,
178
+ ),
179
+ };
180
+ }
181
+
182
+ export { applyBeforeSendSpanCallback, contextsToAttributes, safeSetSpanJSONAttributes };
183
+ //# sourceMappingURL=spanFirstUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spanFirstUtils.js","sources":["../../../src/spans/spanFirstUtils.ts"],"sourcesContent":["import type { RawAttributes } from '../attributes';\nimport { attributeValueToTypedAttributeValue, isAttributeObject } from '../attributes';\nimport type { Context, Contexts } from '../types-hoist/context';\nimport type { SpanV2JSON } from '../types-hoist/span';\nimport { isPrimitive } from '../utils/is';\nimport { showSpanDropWarning } from '../utils/spanUtils';\n\n/**\n * Only set a span JSON attribute if it is not already set.\n * This is used to safely set attributes on JSON objects without mutating already-ended span instances.\n */\nexport function safeSetSpanJSONAttributes(\n spanJSON: SpanV2JSON,\n newAttributes: RawAttributes<Record<string, unknown>>,\n): void {\n if (!spanJSON.attributes) {\n spanJSON.attributes = {};\n }\n\n const originalAttributes = spanJSON.attributes;\n\n Object.keys(newAttributes).forEach(key => {\n if (!originalAttributes?.[key]) {\n setAttributeOnSpanJSONWithMaybeUnit(\n // type-casting here because we ensured above that the attributes object exists\n spanJSON as SpanV2JSON & Required<Pick<SpanV2JSON, 'attributes'>>,\n key,\n newAttributes[key],\n );\n }\n });\n}\n\n/**\n * Apply a user-provided beforeSendSpan callback to a span JSON.\n */\nexport function applyBeforeSendSpanCallback(\n span: SpanV2JSON,\n beforeSendSpan: (span: SpanV2JSON) => SpanV2JSON,\n): SpanV2JSON {\n const modifedSpan = beforeSendSpan(span);\n if (!modifedSpan) {\n showSpanDropWarning();\n return span;\n }\n return modifedSpan;\n}\n\nfunction setAttributeOnSpanJSONWithMaybeUnit(\n spanJSON: SpanV2JSON & Required<Pick<SpanV2JSON, 'attributes'>>,\n attributeKey: string,\n attributeValue: unknown,\n): void {\n if (isAttributeObject(attributeValue)) {\n const { value, unit } = attributeValue;\n\n if (isSupportedSerializableType(value)) {\n spanJSON.attributes[attributeKey] = attributeValueToTypedAttributeValue(attributeValue);\n if (unit) {\n spanJSON.attributes[attributeKey].unit = unit;\n }\n }\n } else if (isSupportedSerializableType(attributeValue)) {\n spanJSON.attributes[attributeKey] = attributeValueToTypedAttributeValue(attributeValue);\n }\n}\n\nfunction isSupportedSerializableType(value: unknown): boolean {\n return ['string', 'number', 'boolean'].includes(typeof value) || Array.isArray(value);\n}\n\n// map of attributes->context keys for those attributes that don't correspond 1:1 to the context key\nconst explicitAttributeToContextMapping = {\n 'os.build_id': 'os.build',\n 'app.name': 'app.app_name',\n 'app.identifier': 'app.app_identifier',\n 'app.version': 'app.app_version',\n 'app.memory': 'app.app_memory',\n 'app.start_time': 'app.app_start_time',\n};\n\nconst knownContexts = [\n // set by `nodeContextIntegration`\n 'app',\n 'os',\n 'device',\n 'culture',\n 'cloud_resource',\n 'runtime',\n\n // TODO: These need more thorough checking if they're all setting expected attributes\n\n // set by the `instrumentPostgresJs`\n 'postgresjsConnection',\n // set by `ensureIsWrapped`\n 'missing_instrumentation',\n // set by `nodeProfilingIntegration`\n 'profile',\n // set by angular `init`\n 'angular',\n // set by AWS Lambda SDK\n 'aws.lambda',\n 'aws.cloudwatch.logs',\n // set by `instrumentBunServe`\n 'response',\n // set by `trpcMiddleware`\n 'trpc',\n // set by `instrumentSupabaseClient`\n 'supabase',\n // set by `gcp.function.context`\n 'gcp.function.context',\n // set by nextjs SDK\n 'nextjs',\n // set by react SDK `captureReactException`, `init`\n 'react',\n // set by react SDK `createReduxEnhancer`\n 'state',\n // set by `replayIntegration`\n 'Replays',\n // Other information:\n // - no need to handler feature flags `flags` context because it's already added to the active span\n];\n\n/**\n * Converts a context object to a set of attributes.\n * Only includes attributes that are primitives (for now).\n * @param contexts - The context object to convert.\n * @returns The attributes object.\n */\nexport function contextsToAttributes(contexts: Contexts): RawAttributes<Record<string, unknown>> {\n function contextToAttribute(context: Context): Context {\n return Object.keys(context).reduce(\n (acc, key) => {\n if (!isPrimitive(context[key])) {\n return acc;\n }\n acc[key] = context[key];\n return acc;\n },\n {} as Record<string, unknown>,\n );\n }\n\n const contextsWithPrimitiveValues = Object.keys(contexts).reduce((acc, key) => {\n if (!knownContexts.includes(key)) {\n return acc;\n }\n const context = contexts[key];\n if (context) {\n acc[key] = contextToAttribute(context);\n }\n return acc;\n }, {} as Contexts);\n\n const explicitlyMappedAttributes = Object.entries(explicitAttributeToContextMapping).reduce(\n (acc, [attributeKey, contextKey]) => {\n const [contextName, contextValueKey] = contextKey.split('.');\n if (contextName && contextValueKey && contextsWithPrimitiveValues[contextName]?.[contextValueKey]) {\n acc[attributeKey] = contextsWithPrimitiveValues[contextName]?.[contextValueKey];\n // now we delete this key from `contextsWithPrimitiveValues` so we don't include it in the next step\n delete contextsWithPrimitiveValues[contextName]?.[contextValueKey];\n }\n return acc;\n },\n {} as Record<string, unknown>,\n );\n\n return {\n ...explicitlyMappedAttributes,\n ...Object.entries(contextsWithPrimitiveValues).reduce(\n (acc, [contextName, contextObj]) => {\n contextObj &&\n Object.entries(contextObj).forEach(([key, value]) => {\n if (value) {\n acc[`${contextName}.${key}`] = value;\n }\n });\n return acc;\n },\n {} as Record<string, unknown>,\n ),\n };\n}\n"],"names":[],"mappings":"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"}
@@ -65,7 +65,8 @@ function getPartText(part) {
65
65
  if (typeof part === 'string') {
66
66
  return part;
67
67
  }
68
- return part.text;
68
+ if ('text' in part) return part.text;
69
+ return '';
69
70
  }
70
71
 
71
72
  /**
@@ -94,6 +95,43 @@ function isContentMessage(message) {
94
95
  );
95
96
  }
96
97
 
98
+ /**
99
+ * Check if a message has the OpenAI/Anthropic content array format.
100
+ */
101
+ function isContentArrayMessage(message) {
102
+ return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content);
103
+ }
104
+
105
+ /**
106
+ * Check if a content part is an OpenAI/Anthropic media source
107
+ */
108
+ function isContentMedia(part) {
109
+ if (!part || typeof part !== 'object') return false;
110
+
111
+ return (
112
+ isContentMediaSource(part) ||
113
+ hasInlineData(part) ||
114
+ ('media_type' in part && typeof part.media_type === 'string' && 'data' in part) ||
115
+ ('image_url' in part && typeof part.image_url === 'string' && part.image_url.startsWith('data:')) ||
116
+ ('type' in part && (part.type === 'blob' || part.type === 'base64')) ||
117
+ 'b64_json' in part ||
118
+ ('type' in part && 'result' in part && part.type === 'image_generation') ||
119
+ ('uri' in part && typeof part.uri === 'string' && part.uri.startsWith('data:'))
120
+ );
121
+ }
122
+ function isContentMediaSource(part) {
123
+ return 'type' in part && typeof part.type === 'string' && 'source' in part && isContentMedia(part.source);
124
+ }
125
+ function hasInlineData(part) {
126
+ return (
127
+ 'inlineData' in part &&
128
+ !!part.inlineData &&
129
+ typeof part.inlineData === 'object' &&
130
+ 'data' in part.inlineData &&
131
+ typeof part.inlineData.data === 'string'
132
+ );
133
+ }
134
+
97
135
  /**
98
136
  * Check if a message has the Google GenAI parts format.
99
137
  */
@@ -172,7 +210,14 @@ function truncatePartsMessage(message, maxBytes) {
172
210
  }
173
211
  }
174
212
 
175
- return includedParts.length > 0 ? [{ ...message, parts: includedParts }] : [];
213
+ /* c8 ignore start
214
+ * for type safety only, algorithm guarantees SOME text included */
215
+ if (includedParts.length <= 0) {
216
+ return [];
217
+ } else {
218
+ /* c8 ignore stop */
219
+ return [{ ...message, parts: includedParts }];
220
+ }
176
221
  }
177
222
 
178
223
  /**
@@ -187,9 +232,11 @@ function truncatePartsMessage(message, maxBytes) {
187
232
  * @returns Array containing the truncated message, or empty array if truncation fails
188
233
  */
189
234
  function truncateSingleMessage(message, maxBytes) {
235
+ /* c8 ignore start - unreachable */
190
236
  if (!message || typeof message !== 'object') {
191
237
  return [];
192
238
  }
239
+ /* c8 ignore stop */
193
240
 
194
241
  if (isContentMessage(message)) {
195
242
  return truncateContentMessage(message, maxBytes);
@@ -203,6 +250,64 @@ function truncateSingleMessage(message, maxBytes) {
203
250
  return [];
204
251
  }
205
252
 
253
+ const REMOVED_STRING = '[Filtered]';
254
+
255
+ const MEDIA_FIELDS = ['image_url', 'data', 'content', 'b64_json', 'result', 'uri'] ;
256
+
257
+ function stripInlineMediaFromSingleMessage(part) {
258
+ const strip = { ...part };
259
+ if (isContentMedia(strip.source)) {
260
+ strip.source = stripInlineMediaFromSingleMessage(strip.source);
261
+ }
262
+ // google genai inline data blob objects
263
+ if (hasInlineData(part)) {
264
+ strip.inlineData = { ...part.inlineData, data: REMOVED_STRING };
265
+ }
266
+ for (const field of MEDIA_FIELDS) {
267
+ if (typeof strip[field] === 'string') strip[field] = REMOVED_STRING;
268
+ }
269
+ return strip;
270
+ }
271
+
272
+ /**
273
+ * Strip the inline media from message arrays.
274
+ *
275
+ * This returns a stripped message. We do NOT want to mutate the data in place,
276
+ * because of course we still want the actual API/client to handle the media.
277
+ */
278
+ function stripInlineMediaFromMessages(messages) {
279
+ const stripped = messages.map(message => {
280
+ let newMessage = undefined;
281
+ if (!!message && typeof message === 'object') {
282
+ if (isContentArrayMessage(message)) {
283
+ newMessage = {
284
+ ...message,
285
+ content: stripInlineMediaFromMessages(message.content),
286
+ };
287
+ } else if ('content' in message && isContentMedia(message.content)) {
288
+ newMessage = {
289
+ ...message,
290
+ content: stripInlineMediaFromSingleMessage(message.content),
291
+ };
292
+ }
293
+ if (isPartsMessage(message)) {
294
+ newMessage = {
295
+ // might have to strip content AND parts
296
+ ...(newMessage ?? message),
297
+ parts: stripInlineMediaFromMessages(message.parts),
298
+ };
299
+ }
300
+ if (isContentMedia(newMessage)) {
301
+ newMessage = stripInlineMediaFromSingleMessage(newMessage);
302
+ } else if (isContentMedia(message)) {
303
+ newMessage = stripInlineMediaFromSingleMessage(message);
304
+ }
305
+ }
306
+ return newMessage ?? message;
307
+ });
308
+ return stripped;
309
+ }
310
+
206
311
  /**
207
312
  * Truncate an array of messages to fit within a byte limit.
208
313
  *
@@ -228,20 +333,24 @@ function truncateMessagesByBytes(messages, maxBytes) {
228
333
  return messages;
229
334
  }
230
335
 
336
+ // strip inline media first. This will often get us below the threshold,
337
+ // while preserving human-readable information about messages sent.
338
+ const stripped = stripInlineMediaFromMessages(messages);
339
+
231
340
  // Fast path: if all messages fit, return as-is
232
- const totalBytes = jsonBytes(messages);
341
+ const totalBytes = jsonBytes(stripped);
233
342
  if (totalBytes <= maxBytes) {
234
- return messages;
343
+ return stripped;
235
344
  }
236
345
 
237
346
  // Precompute each message's JSON size once for efficiency
238
- const messageSizes = messages.map(jsonBytes);
347
+ const messageSizes = stripped.map(jsonBytes);
239
348
 
240
349
  // Find the largest suffix (newest messages) that fits within the budget
241
350
  let bytesUsed = 0;
242
- let startIndex = messages.length; // Index where the kept suffix starts
351
+ let startIndex = stripped.length; // Index where the kept suffix starts
243
352
 
244
- for (let i = messages.length - 1; i >= 0; i--) {
353
+ for (let i = stripped.length - 1; i >= 0; i--) {
245
354
  const messageSize = messageSizes[i];
246
355
 
247
356
  if (messageSize && bytesUsed + messageSize > maxBytes) {
@@ -256,13 +365,14 @@ function truncateMessagesByBytes(messages, maxBytes) {
256
365
  }
257
366
 
258
367
  // If no complete messages fit, try truncating just the newest message
259
- if (startIndex === messages.length) {
260
- const newestMessage = messages[messages.length - 1];
368
+ if (startIndex === stripped.length) {
369
+ // we're truncating down to one message, so all others dropped.
370
+ const newestMessage = stripped[stripped.length - 1];
261
371
  return truncateSingleMessage(newestMessage, maxBytes);
262
372
  }
263
373
 
264
374
  // Return the suffix that fits
265
- return messages.slice(startIndex);
375
+ return stripped.slice(startIndex);
266
376
  }
267
377
 
268
378
  /**
@@ -287,5 +397,5 @@ function truncateGenAiStringInput(input) {
287
397
  return truncateTextByBytes(input, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT);
288
398
  }
289
399
 
290
- export { DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT, truncateGenAiMessages, truncateGenAiStringInput, truncateMessagesByBytes };
400
+ export { DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT, truncateGenAiMessages, truncateGenAiStringInput };
291
401
  //# sourceMappingURL=messageTruncation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messageTruncation.js","sources":["../../../../src/tracing/ai/messageTruncation.ts"],"sourcesContent":["/**\n * Default maximum size in bytes for GenAI messages.\n * Messages exceeding this limit will be truncated.\n */\nexport const DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT = 20000;\n\n/**\n * Message format used by OpenAI and Anthropic APIs.\n */\ntype ContentMessage = {\n [key: string]: unknown;\n content: string;\n};\n\n/**\n * Message format used by Google GenAI API.\n * Parts can be strings or objects with a text property.\n */\ntype PartsMessage = {\n [key: string]: unknown;\n parts: Array<string | { text: string }>;\n};\n\n/**\n * A part in a Google GenAI message that contains text.\n */\ntype TextPart = string | { text: string };\n\n/**\n * Calculate the UTF-8 byte length of a string.\n */\nconst utf8Bytes = (text: string): number => {\n return new TextEncoder().encode(text).length;\n};\n\n/**\n * Calculate the UTF-8 byte length of a value's JSON representation.\n */\nconst jsonBytes = (value: unknown): number => {\n return utf8Bytes(JSON.stringify(value));\n};\n\n/**\n * Truncate a string to fit within maxBytes when encoded as UTF-8.\n * Uses binary search for efficiency with multi-byte characters.\n *\n * @param text - The string to truncate\n * @param maxBytes - Maximum byte length (UTF-8 encoded)\n * @returns Truncated string that fits within maxBytes\n */\nfunction truncateTextByBytes(text: string, maxBytes: number): string {\n if (utf8Bytes(text) <= maxBytes) {\n return text;\n }\n\n let low = 0;\n let high = text.length;\n let bestFit = '';\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const candidate = text.slice(0, mid);\n const byteSize = utf8Bytes(candidate);\n\n if (byteSize <= maxBytes) {\n bestFit = candidate;\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n return bestFit;\n}\n\n/**\n * Extract text content from a Google GenAI message part.\n * Parts are either plain strings or objects with a text property.\n *\n * @returns The text content\n */\nfunction getPartText(part: TextPart): string {\n if (typeof part === 'string') {\n return part;\n }\n return part.text;\n}\n\n/**\n * Create a new part with updated text content while preserving the original structure.\n *\n * @param part - Original part (string or object)\n * @param text - New text content\n * @returns New part with updated text\n */\nfunction withPartText(part: TextPart, text: string): TextPart {\n if (typeof part === 'string') {\n return text;\n }\n return { ...part, text };\n}\n\n/**\n * Check if a message has the OpenAI/Anthropic content format.\n */\nfunction isContentMessage(message: unknown): message is ContentMessage {\n return (\n message !== null &&\n typeof message === 'object' &&\n 'content' in message &&\n typeof (message as ContentMessage).content === 'string'\n );\n}\n\n/**\n * Check if a message has the Google GenAI parts format.\n */\nfunction isPartsMessage(message: unknown): message is PartsMessage {\n return (\n message !== null &&\n typeof message === 'object' &&\n 'parts' in message &&\n Array.isArray((message as PartsMessage).parts) &&\n (message as PartsMessage).parts.length > 0\n );\n}\n\n/**\n * Truncate a message with `content: string` format (OpenAI/Anthropic).\n *\n * @param message - Message with content property\n * @param maxBytes - Maximum byte limit\n * @returns Array with truncated message, or empty array if it doesn't fit\n */\nfunction truncateContentMessage(message: ContentMessage, maxBytes: number): unknown[] {\n // Calculate overhead (message structure without content)\n const emptyMessage = { ...message, content: '' };\n const overhead = jsonBytes(emptyMessage);\n const availableForContent = maxBytes - overhead;\n\n if (availableForContent <= 0) {\n return [];\n }\n\n const truncatedContent = truncateTextByBytes(message.content, availableForContent);\n return [{ ...message, content: truncatedContent }];\n}\n\n/**\n * Truncate a message with `parts: [...]` format (Google GenAI).\n * Keeps as many complete parts as possible, only truncating the first part if needed.\n *\n * @param message - Message with parts array\n * @param maxBytes - Maximum byte limit\n * @returns Array with truncated message, or empty array if it doesn't fit\n */\nfunction truncatePartsMessage(message: PartsMessage, maxBytes: number): unknown[] {\n const { parts } = message;\n\n // Calculate overhead by creating empty text parts\n const emptyParts = parts.map(part => withPartText(part, ''));\n const overhead = jsonBytes({ ...message, parts: emptyParts });\n let remainingBytes = maxBytes - overhead;\n\n if (remainingBytes <= 0) {\n return [];\n }\n\n // Include parts until we run out of space\n const includedParts: TextPart[] = [];\n\n for (const part of parts) {\n const text = getPartText(part);\n const textSize = utf8Bytes(text);\n\n if (textSize <= remainingBytes) {\n // Part fits: include it as-is\n includedParts.push(part);\n remainingBytes -= textSize;\n } else if (includedParts.length === 0) {\n // First part doesn't fit: truncate it\n const truncated = truncateTextByBytes(text, remainingBytes);\n if (truncated) {\n includedParts.push(withPartText(part, truncated));\n }\n break;\n } else {\n // Subsequent part doesn't fit: stop here\n break;\n }\n }\n\n return includedParts.length > 0 ? [{ ...message, parts: includedParts }] : [];\n}\n\n/**\n * Truncate a single message to fit within maxBytes.\n *\n * Supports two message formats:\n * - OpenAI/Anthropic: `{ ..., content: string }`\n * - Google GenAI: `{ ..., parts: Array<string | {text: string} | non-text> }`\n *\n * @param message - The message to truncate\n * @param maxBytes - Maximum byte limit for the message\n * @returns Array containing the truncated message, or empty array if truncation fails\n */\nfunction truncateSingleMessage(message: unknown, maxBytes: number): unknown[] {\n if (!message || typeof message !== 'object') {\n return [];\n }\n\n if (isContentMessage(message)) {\n return truncateContentMessage(message, maxBytes);\n }\n\n if (isPartsMessage(message)) {\n return truncatePartsMessage(message, maxBytes);\n }\n\n // Unknown message format: cannot truncate safely\n return [];\n}\n\n/**\n * Truncate an array of messages to fit within a byte limit.\n *\n * Strategy:\n * - Keeps the newest messages (from the end of the array)\n * - Uses O(n) algorithm: precompute sizes once, then find largest suffix under budget\n * - If no complete messages fit, attempts to truncate the newest single message\n *\n * @param messages - Array of messages to truncate\n * @param maxBytes - Maximum total byte limit for all messages\n * @returns Truncated array of messages\n *\n * @example\n * ```ts\n * const messages = [msg1, msg2, msg3, msg4]; // newest is msg4\n * const truncated = truncateMessagesByBytes(messages, 10000);\n * // Returns [msg3, msg4] if they fit, or [msg4] if only it fits, etc.\n * ```\n */\nexport function truncateMessagesByBytes(messages: unknown[], maxBytes: number): unknown[] {\n // Early return for empty or invalid input\n if (!Array.isArray(messages) || messages.length === 0) {\n return messages;\n }\n\n // Fast path: if all messages fit, return as-is\n const totalBytes = jsonBytes(messages);\n if (totalBytes <= maxBytes) {\n return messages;\n }\n\n // Precompute each message's JSON size once for efficiency\n const messageSizes = messages.map(jsonBytes);\n\n // Find the largest suffix (newest messages) that fits within the budget\n let bytesUsed = 0;\n let startIndex = messages.length; // Index where the kept suffix starts\n\n for (let i = messages.length - 1; i >= 0; i--) {\n const messageSize = messageSizes[i];\n\n if (messageSize && bytesUsed + messageSize > maxBytes) {\n // Adding this message would exceed the budget\n break;\n }\n\n if (messageSize) {\n bytesUsed += messageSize;\n }\n startIndex = i;\n }\n\n // If no complete messages fit, try truncating just the newest message\n if (startIndex === messages.length) {\n const newestMessage = messages[messages.length - 1];\n return truncateSingleMessage(newestMessage, maxBytes);\n }\n\n // Return the suffix that fits\n return messages.slice(startIndex);\n}\n\n/**\n * Truncate GenAI messages using the default byte limit.\n *\n * Convenience wrapper around `truncateMessagesByBytes` with the default limit.\n *\n * @param messages - Array of messages to truncate\n * @returns Truncated array of messages\n */\nexport function truncateGenAiMessages(messages: unknown[]): unknown[] {\n return truncateMessagesByBytes(messages, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT);\n}\n\n/**\n * Truncate GenAI string input using the default byte limit.\n *\n * @param input - The string to truncate\n * @returns Truncated string\n */\nexport function truncateGenAiStringInput(input: string): string {\n return truncateTextByBytes(input, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"messageTruncation.js","sources":["../../../../src/tracing/ai/messageTruncation.ts"],"sourcesContent":["/**\n * Default maximum size in bytes for GenAI messages.\n * Messages exceeding this limit will be truncated.\n */\nexport const DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT = 20000;\n\n/**\n * Message format used by OpenAI and Anthropic APIs.\n */\ntype ContentMessage = {\n [key: string]: unknown;\n content: string;\n};\n\n/**\n * Message format used by OpenAI and Anthropic APIs for media.\n */\ntype ContentArrayMessage = {\n [key: string]: unknown;\n content: {\n [key: string]: unknown;\n type: string;\n }[];\n};\n\n/**\n * Inline media content source, with a potentially very large base64\n * blob or data: uri.\n */\ntype ContentMedia = Record<string, unknown> &\n (\n | {\n media_type: string;\n data: string;\n }\n | {\n image_url: `data:${string}`;\n }\n | {\n type: 'blob' | 'base64';\n content: string;\n }\n | {\n b64_json: string;\n }\n | {\n uri: `data:${string}`;\n }\n );\n\n/**\n * Message format used by Google GenAI API.\n * Parts can be strings or objects with a text property.\n */\ntype PartsMessage = {\n [key: string]: unknown;\n parts: Array<TextPart | MediaPart>;\n};\n\n/**\n * A part in a Google GenAI message that contains text.\n */\ntype TextPart = string | { text: string };\n\n/**\n * A part in a Google GenAI that contains media.\n */\ntype MediaPart = {\n type: string;\n content: string;\n};\n\n/**\n * Calculate the UTF-8 byte length of a string.\n */\nconst utf8Bytes = (text: string): number => {\n return new TextEncoder().encode(text).length;\n};\n\n/**\n * Calculate the UTF-8 byte length of a value's JSON representation.\n */\nconst jsonBytes = (value: unknown): number => {\n return utf8Bytes(JSON.stringify(value));\n};\n\n/**\n * Truncate a string to fit within maxBytes when encoded as UTF-8.\n * Uses binary search for efficiency with multi-byte characters.\n *\n * @param text - The string to truncate\n * @param maxBytes - Maximum byte length (UTF-8 encoded)\n * @returns Truncated string that fits within maxBytes\n */\nfunction truncateTextByBytes(text: string, maxBytes: number): string {\n if (utf8Bytes(text) <= maxBytes) {\n return text;\n }\n\n let low = 0;\n let high = text.length;\n let bestFit = '';\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const candidate = text.slice(0, mid);\n const byteSize = utf8Bytes(candidate);\n\n if (byteSize <= maxBytes) {\n bestFit = candidate;\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n return bestFit;\n}\n\n/**\n * Extract text content from a Google GenAI message part.\n * Parts are either plain strings or objects with a text property.\n *\n * @returns The text content\n */\nfunction getPartText(part: TextPart | MediaPart): string {\n if (typeof part === 'string') {\n return part;\n }\n if ('text' in part) return part.text;\n return '';\n}\n\n/**\n * Create a new part with updated text content while preserving the original structure.\n *\n * @param part - Original part (string or object)\n * @param text - New text content\n * @returns New part with updated text\n */\nfunction withPartText(part: TextPart | MediaPart, text: string): TextPart {\n if (typeof part === 'string') {\n return text;\n }\n return { ...part, text };\n}\n\n/**\n * Check if a message has the OpenAI/Anthropic content format.\n */\nfunction isContentMessage(message: unknown): message is ContentMessage {\n return (\n message !== null &&\n typeof message === 'object' &&\n 'content' in message &&\n typeof (message as ContentMessage).content === 'string'\n );\n}\n\n/**\n * Check if a message has the OpenAI/Anthropic content array format.\n */\nfunction isContentArrayMessage(message: unknown): message is ContentArrayMessage {\n return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content);\n}\n\n/**\n * Check if a content part is an OpenAI/Anthropic media source\n */\nfunction isContentMedia(part: unknown): part is ContentMedia {\n if (!part || typeof part !== 'object') return false;\n\n return (\n isContentMediaSource(part) ||\n hasInlineData(part) ||\n ('media_type' in part && typeof part.media_type === 'string' && 'data' in part) ||\n ('image_url' in part && typeof part.image_url === 'string' && part.image_url.startsWith('data:')) ||\n ('type' in part && (part.type === 'blob' || part.type === 'base64')) ||\n 'b64_json' in part ||\n ('type' in part && 'result' in part && part.type === 'image_generation') ||\n ('uri' in part && typeof part.uri === 'string' && part.uri.startsWith('data:'))\n );\n}\nfunction isContentMediaSource(part: NonNullable<unknown>): boolean {\n return 'type' in part && typeof part.type === 'string' && 'source' in part && isContentMedia(part.source);\n}\nfunction hasInlineData(part: NonNullable<unknown>): part is { inlineData: { data?: string } } {\n return (\n 'inlineData' in part &&\n !!part.inlineData &&\n typeof part.inlineData === 'object' &&\n 'data' in part.inlineData &&\n typeof part.inlineData.data === 'string'\n );\n}\n\n/**\n * Check if a message has the Google GenAI parts format.\n */\nfunction isPartsMessage(message: unknown): message is PartsMessage {\n return (\n message !== null &&\n typeof message === 'object' &&\n 'parts' in message &&\n Array.isArray((message as PartsMessage).parts) &&\n (message as PartsMessage).parts.length > 0\n );\n}\n\n/**\n * Truncate a message with `content: string` format (OpenAI/Anthropic).\n *\n * @param message - Message with content property\n * @param maxBytes - Maximum byte limit\n * @returns Array with truncated message, or empty array if it doesn't fit\n */\nfunction truncateContentMessage(message: ContentMessage, maxBytes: number): unknown[] {\n // Calculate overhead (message structure without content)\n const emptyMessage = { ...message, content: '' };\n const overhead = jsonBytes(emptyMessage);\n const availableForContent = maxBytes - overhead;\n\n if (availableForContent <= 0) {\n return [];\n }\n\n const truncatedContent = truncateTextByBytes(message.content, availableForContent);\n return [{ ...message, content: truncatedContent }];\n}\n\n/**\n * Truncate a message with `parts: [...]` format (Google GenAI).\n * Keeps as many complete parts as possible, only truncating the first part if needed.\n *\n * @param message - Message with parts array\n * @param maxBytes - Maximum byte limit\n * @returns Array with truncated message, or empty array if it doesn't fit\n */\nfunction truncatePartsMessage(message: PartsMessage, maxBytes: number): unknown[] {\n const { parts } = message;\n\n // Calculate overhead by creating empty text parts\n const emptyParts = parts.map(part => withPartText(part, ''));\n const overhead = jsonBytes({ ...message, parts: emptyParts });\n let remainingBytes = maxBytes - overhead;\n\n if (remainingBytes <= 0) {\n return [];\n }\n\n // Include parts until we run out of space\n const includedParts: (TextPart | MediaPart)[] = [];\n\n for (const part of parts) {\n const text = getPartText(part);\n const textSize = utf8Bytes(text);\n\n if (textSize <= remainingBytes) {\n // Part fits: include it as-is\n includedParts.push(part);\n remainingBytes -= textSize;\n } else if (includedParts.length === 0) {\n // First part doesn't fit: truncate it\n const truncated = truncateTextByBytes(text, remainingBytes);\n if (truncated) {\n includedParts.push(withPartText(part, truncated));\n }\n break;\n } else {\n // Subsequent part doesn't fit: stop here\n break;\n }\n }\n\n /* c8 ignore start\n * for type safety only, algorithm guarantees SOME text included */\n if (includedParts.length <= 0) {\n return [];\n } else {\n /* c8 ignore stop */\n return [{ ...message, parts: includedParts }];\n }\n}\n\n/**\n * Truncate a single message to fit within maxBytes.\n *\n * Supports two message formats:\n * - OpenAI/Anthropic: `{ ..., content: string }`\n * - Google GenAI: `{ ..., parts: Array<string | {text: string} | non-text> }`\n *\n * @param message - The message to truncate\n * @param maxBytes - Maximum byte limit for the message\n * @returns Array containing the truncated message, or empty array if truncation fails\n */\nfunction truncateSingleMessage(message: unknown, maxBytes: number): unknown[] {\n /* c8 ignore start - unreachable */\n if (!message || typeof message !== 'object') {\n return [];\n }\n /* c8 ignore stop */\n\n if (isContentMessage(message)) {\n return truncateContentMessage(message, maxBytes);\n }\n\n if (isPartsMessage(message)) {\n return truncatePartsMessage(message, maxBytes);\n }\n\n // Unknown message format: cannot truncate safely\n return [];\n}\n\nconst REMOVED_STRING = '[Filtered]';\n\nconst MEDIA_FIELDS = ['image_url', 'data', 'content', 'b64_json', 'result', 'uri'] as const;\n\nfunction stripInlineMediaFromSingleMessage(part: ContentMedia): ContentMedia {\n const strip = { ...part };\n if (isContentMedia(strip.source)) {\n strip.source = stripInlineMediaFromSingleMessage(strip.source);\n }\n // google genai inline data blob objects\n if (hasInlineData(part)) {\n strip.inlineData = { ...part.inlineData, data: REMOVED_STRING };\n }\n for (const field of MEDIA_FIELDS) {\n if (typeof strip[field] === 'string') strip[field] = REMOVED_STRING;\n }\n return strip;\n}\n\n/**\n * Strip the inline media from message arrays.\n *\n * This returns a stripped message. We do NOT want to mutate the data in place,\n * because of course we still want the actual API/client to handle the media.\n */\nfunction stripInlineMediaFromMessages(messages: unknown[]): unknown[] {\n const stripped = messages.map(message => {\n let newMessage: Record<string, unknown> | undefined = undefined;\n if (!!message && typeof message === 'object') {\n if (isContentArrayMessage(message)) {\n newMessage = {\n ...message,\n content: stripInlineMediaFromMessages(message.content),\n };\n } else if ('content' in message && isContentMedia(message.content)) {\n newMessage = {\n ...message,\n content: stripInlineMediaFromSingleMessage(message.content),\n };\n }\n if (isPartsMessage(message)) {\n newMessage = {\n // might have to strip content AND parts\n ...(newMessage ?? message),\n parts: stripInlineMediaFromMessages(message.parts),\n };\n }\n if (isContentMedia(newMessage)) {\n newMessage = stripInlineMediaFromSingleMessage(newMessage);\n } else if (isContentMedia(message)) {\n newMessage = stripInlineMediaFromSingleMessage(message);\n }\n }\n return newMessage ?? message;\n });\n return stripped;\n}\n\n/**\n * Truncate an array of messages to fit within a byte limit.\n *\n * Strategy:\n * - Keeps the newest messages (from the end of the array)\n * - Uses O(n) algorithm: precompute sizes once, then find largest suffix under budget\n * - If no complete messages fit, attempts to truncate the newest single message\n *\n * @param messages - Array of messages to truncate\n * @param maxBytes - Maximum total byte limit for all messages\n * @returns Truncated array of messages\n *\n * @example\n * ```ts\n * const messages = [msg1, msg2, msg3, msg4]; // newest is msg4\n * const truncated = truncateMessagesByBytes(messages, 10000);\n * // Returns [msg3, msg4] if they fit, or [msg4] if only it fits, etc.\n * ```\n */\nfunction truncateMessagesByBytes(messages: unknown[], maxBytes: number): unknown[] {\n // Early return for empty or invalid input\n if (!Array.isArray(messages) || messages.length === 0) {\n return messages;\n }\n\n // strip inline media first. This will often get us below the threshold,\n // while preserving human-readable information about messages sent.\n const stripped = stripInlineMediaFromMessages(messages);\n\n // Fast path: if all messages fit, return as-is\n const totalBytes = jsonBytes(stripped);\n if (totalBytes <= maxBytes) {\n return stripped;\n }\n\n // Precompute each message's JSON size once for efficiency\n const messageSizes = stripped.map(jsonBytes);\n\n // Find the largest suffix (newest messages) that fits within the budget\n let bytesUsed = 0;\n let startIndex = stripped.length; // Index where the kept suffix starts\n\n for (let i = stripped.length - 1; i >= 0; i--) {\n const messageSize = messageSizes[i];\n\n if (messageSize && bytesUsed + messageSize > maxBytes) {\n // Adding this message would exceed the budget\n break;\n }\n\n if (messageSize) {\n bytesUsed += messageSize;\n }\n startIndex = i;\n }\n\n // If no complete messages fit, try truncating just the newest message\n if (startIndex === stripped.length) {\n // we're truncating down to one message, so all others dropped.\n const newestMessage = stripped[stripped.length - 1];\n return truncateSingleMessage(newestMessage, maxBytes);\n }\n\n // Return the suffix that fits\n return stripped.slice(startIndex);\n}\n\n/**\n * Truncate GenAI messages using the default byte limit.\n *\n * Convenience wrapper around `truncateMessagesByBytes` with the default limit.\n *\n * @param messages - Array of messages to truncate\n * @returns Truncated array of messages\n */\nexport function truncateGenAiMessages(messages: unknown[]): unknown[] {\n return truncateMessagesByBytes(messages, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT);\n}\n\n/**\n * Truncate GenAI string input using the default byte limit.\n *\n * @param input - The string to truncate\n * @returns Truncated string\n */\nexport function truncateGenAiStringInput(input: string): string {\n return truncateTextByBytes(input, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT);\n}\n"],"names":[],"mappings":"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"}
@@ -4,11 +4,12 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes.js';
4
4
  import { SPAN_STATUS_ERROR } from '../spanstatus.js';
5
5
  import { startSpanManual, startSpan } from '../trace.js';
6
6
  import { handleCallbackErrors } from '../../utils/handleCallbackErrors.js';
7
- import { GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MESSAGES_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, GEN_AI_REQUEST_TOP_P_ATTRIBUTE, GEN_AI_REQUEST_TOP_K_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE } from '../ai/gen-ai-attributes.js';
8
- import { buildMethodPath, getFinalOperationName, getSpanOperation, getTruncatedJsonString } from '../ai/utils.js';
7
+ import { GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MESSAGES_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, GEN_AI_REQUEST_TOP_P_ATTRIBUTE, GEN_AI_REQUEST_TOP_K_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE } from '../ai/gen-ai-attributes.js';
8
+ import { truncateGenAiMessages } from '../ai/messageTruncation.js';
9
+ import { buildMethodPath, getFinalOperationName, getSpanOperation } from '../ai/utils.js';
9
10
  import { CHATS_CREATE_METHOD, CHAT_PATH, GOOGLE_GENAI_SYSTEM_NAME } from './constants.js';
10
11
  import { instrumentStream } from './streaming.js';
11
- import { shouldInstrument, isStreamingMethod } from './utils.js';
12
+ import { shouldInstrument, isStreamingMethod, contentUnionToMessages } from './utils.js';
12
13
 
13
14
  /**
14
15
  * Extract model from parameters or chat context object
@@ -109,26 +110,38 @@ function extractRequestAttributes(
109
110
  * Handles different parameter formats for different Google GenAI methods.
110
111
  */
111
112
  function addPrivateRequestAttributes(span, params) {
112
- // For models.generateContent: ContentListUnion: Content | Content[] | PartUnion | PartUnion[]
113
+ const messages = [];
114
+
115
+ // config.systemInstruction: ContentUnion
116
+ if (
117
+ 'config' in params &&
118
+ params.config &&
119
+ typeof params.config === 'object' &&
120
+ 'systemInstruction' in params.config &&
121
+ params.config.systemInstruction
122
+ ) {
123
+ messages.push(...contentUnionToMessages(params.config.systemInstruction , 'system'));
124
+ }
125
+
126
+ // For chats.create: history contains the conversation history
127
+ if ('history' in params) {
128
+ messages.push(...contentUnionToMessages(params.history , 'user'));
129
+ }
130
+
131
+ // For models.generateContent: ContentListUnion
113
132
  if ('contents' in params) {
114
- const contents = params.contents;
115
- // For models.generateContent: ContentListUnion: Content | Content[] | PartUnion | PartUnion[]
116
- const truncatedContents = getTruncatedJsonString(contents);
117
- span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: truncatedContents });
133
+ messages.push(...contentUnionToMessages(params.contents , 'user'));
118
134
  }
119
135
 
120
- // For chat.sendMessage: message can be string or Part[]
136
+ // For chat.sendMessage: message can be PartListUnion
121
137
  if ('message' in params) {
122
- const message = params.message;
123
- const truncatedMessage = getTruncatedJsonString(message);
124
- span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: truncatedMessage });
138
+ messages.push(...contentUnionToMessages(params.message , 'user'));
125
139
  }
126
140
 
127
- // For chats.create: history contains the conversation history
128
- if ('history' in params) {
129
- const history = params.history;
130
- const truncatedHistory = getTruncatedJsonString(history);
131
- span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: truncatedHistory });
141
+ if (messages.length) {
142
+ span.setAttributes({
143
+ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(truncateGenAiMessages(messages)),
144
+ });
132
145
  }
133
146
  }
134
147
 
@@ -139,6 +152,10 @@ function addPrivateRequestAttributes(span, params) {
139
152
  function addResponseAttributes(span, response, recordOutputs) {
140
153
  if (!response || typeof response !== 'object') return;
141
154
 
155
+ if (response.modelVersion) {
156
+ span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, response.modelVersion);
157
+ }
158
+
142
159
  // Add usage metadata if present
143
160
  if (response.usageMetadata && typeof response.usageMetadata === 'object') {
144
161
  const usage = response.usageMetadata;