-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathinternal.ts
More file actions
261 lines (227 loc) · 9.93 KB
/
internal.ts
File metadata and controls
261 lines (227 loc) · 9.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import type { Attributes } from '../attributes';
import { serializeAttributes } from '../attributes';
import { getGlobalSingleton } from '../carrier';
import type { Client } from '../client';
import { getClient, getCurrentScope, getIsolationScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';
import type { Integration } from '../types-hoist/integration';
import type { Log, SerializedLog } from '../types-hoist/log';
import { consoleSandbox, debug } from '../utils/debug-logger';
import { isParameterizedString } from '../utils/is';
import { getCombinedScopeData } from '../utils/scopeData';
import { _getSpanForScope } from '../utils/spanOnScope';
import { timestampInSeconds } from '../utils/time';
import { getSequenceAttribute } from '../utils/timestampSequence';
import { _getTraceInfoFromScope } from '../utils/trace-info';
import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants';
import { createLogEnvelope } from './envelope';
const MAX_LOG_BUFFER_SIZE = 100;
/**
* Sets a log attribute if the value exists and the attribute key is not already present.
*
* @param logAttributes - The log attributes object to modify.
* @param key - The attribute key to set.
* @param value - The value to set (only sets if truthy and key not present).
* @param setEvenIfPresent - Whether to set the attribute if it is present. Defaults to true.
*/
function setLogAttribute(
logAttributes: Record<string, unknown>,
key: string,
value: unknown,
setEvenIfPresent = true,
): void {
if (value && (!logAttributes[key] || setEvenIfPresent)) {
logAttributes[key] = value;
}
}
/**
* Captures a serialized log event and adds it to the log buffer for the given client.
*
* @param client - A client. Uses the current client if not provided.
* @param serializedLog - The serialized log event to capture.
*
* @experimental This method will experience breaking changes. This is not yet part of
* the stable Sentry SDK API and can be changed or removed without warning.
*/
export function _INTERNAL_captureSerializedLog(client: Client, serializedLog: SerializedLog): void {
const bufferMap = _getBufferMap();
const logBuffer = _INTERNAL_getLogBuffer(client);
if (logBuffer === undefined) {
bufferMap.set(client, [serializedLog]);
} else {
if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) {
_INTERNAL_flushLogsBuffer(client, logBuffer);
bufferMap.set(client, [serializedLog]);
} else {
bufferMap.set(client, [...logBuffer, serializedLog]);
}
}
}
/**
* Captures a log event and sends it to Sentry.
*
* @param log - The log event to capture.
* @param scope - A scope. Uses the current scope if not provided.
* @param client - A client. Uses the current client if not provided.
* @param captureSerializedLog - A function to capture the serialized log.
*
* @experimental This method will experience breaking changes. This is not yet part of
* the stable Sentry SDK API and can be changed or removed without warning.
*/
export function _INTERNAL_captureLog(
beforeLog: Log,
currentScope = getCurrentScope(),
captureSerializedLog: (client: Client, log: SerializedLog) => void = _INTERNAL_captureSerializedLog,
): void {
const client = currentScope?.getClient() ?? getClient();
if (!client) {
DEBUG_BUILD && debug.warn('No client available to capture log.');
return;
}
const { release, environment, enableLogs = false, beforeSendLog } = client.getOptions();
if (!enableLogs) {
DEBUG_BUILD && debug.warn('logging option not enabled, log will not be captured.');
return;
}
const [, traceContext] = _getTraceInfoFromScope(client, currentScope);
const processedLogAttributes = {
...beforeLog.attributes,
};
const {
user: { id, email, username },
attributes: scopeAttributes = {},
} = getCombinedScopeData(getIsolationScope(), currentScope);
setLogAttribute(processedLogAttributes, 'user.id', id, false);
setLogAttribute(processedLogAttributes, 'user.email', email, false);
setLogAttribute(processedLogAttributes, 'user.name', username, false);
setLogAttribute(processedLogAttributes, 'sentry.release', release);
setLogAttribute(processedLogAttributes, 'sentry.environment', environment);
const { name, version } = client.getSdkMetadata()?.sdk ?? {};
setLogAttribute(processedLogAttributes, 'sentry.sdk.name', name);
setLogAttribute(processedLogAttributes, 'sentry.sdk.version', version);
const replay = client.getIntegrationByName<
Integration & {
getReplayId: (onlyIfSampled?: boolean) => string;
getRecordingMode: () => 'session' | 'buffer' | undefined;
}
>('Replay');
const replayId = replay?.getReplayId(true);
setLogAttribute(processedLogAttributes, 'sentry.replay_id', replayId);
if (replayId && replay?.getRecordingMode() === 'buffer') {
// We send this so we can identify cases where the replayId is attached but the replay itself might not have been sent to Sentry
setLogAttribute(processedLogAttributes, 'sentry._internal.replay_is_buffering', true);
}
const beforeLogMessage = beforeLog.message;
if (isParameterizedString(beforeLogMessage)) {
const { __sentry_template_string__, __sentry_template_values__ = [] } = beforeLogMessage;
if (__sentry_template_values__?.length) {
processedLogAttributes['sentry.message.template'] = __sentry_template_string__;
}
__sentry_template_values__.forEach((param, index) => {
processedLogAttributes[`sentry.message.parameter.${index}`] = param;
});
}
const span = _getSpanForScope(currentScope);
// Add the parent span ID to the log attributes for trace context
setLogAttribute(processedLogAttributes, 'sentry.trace.parent_span_id', span?.spanContext().spanId);
const processedLog = { ...beforeLog, attributes: processedLogAttributes };
client.emit('beforeCaptureLog', processedLog);
// We need to wrap this in `consoleSandbox` to avoid recursive calls to `beforeSendLog`
const log = beforeSendLog ? consoleSandbox(() => beforeSendLog(processedLog)) : processedLog;
if (!log) {
client.recordDroppedEvent('before_send', 'log_item', 1);
DEBUG_BUILD && debug.warn('beforeSendLog returned null, log will not be captured.');
return;
}
const { level, message, attributes: logAttributes = {}, severityNumber } = log;
const timestamp = timestampInSeconds();
const sequenceAttr = getSequenceAttribute(timestamp);
const serializedLog: SerializedLog = {
timestamp,
level,
body: _INTERNAL_removeLoneSurrogates(String(message)),
trace_id: traceContext?.trace_id,
severity_number: severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER[level],
attributes: sanitizeLogAttributes({
...serializeAttributes(scopeAttributes),
...serializeAttributes(logAttributes, true),
[sequenceAttr.key]: sequenceAttr.value,
}),
};
captureSerializedLog(client, serializedLog);
client.emit('afterCaptureLog', log);
}
/**
* Flushes the logs buffer to Sentry.
*
* @param client - A client.
* @param maybeLogBuffer - A log buffer. Uses the log buffer for the given client if not provided.
*
* @experimental This method will experience breaking changes. This is not yet part of
* the stable Sentry SDK API and can be changed or removed without warning.
*/
export function _INTERNAL_flushLogsBuffer(client: Client, maybeLogBuffer?: Array<SerializedLog>): void {
const logBuffer = maybeLogBuffer ?? _INTERNAL_getLogBuffer(client) ?? [];
if (logBuffer.length === 0) {
return;
}
const clientOptions = client.getOptions();
const envelope = createLogEnvelope(logBuffer, clientOptions._metadata, clientOptions.tunnel, client.getDsn());
// Clear the log buffer after envelopes have been constructed.
_getBufferMap().set(client, []);
client.emit('flushLogs');
// sendEnvelope should not throw
// eslint-disable-next-line @typescript-eslint/no-floating-promises
client.sendEnvelope(envelope);
}
/**
* Returns the log buffer for a given client.
*
* Exported for testing purposes.
*
* @param client - The client to get the log buffer for.
* @returns The log buffer for the given client.
*/
export function _INTERNAL_getLogBuffer(client: Client): Array<SerializedLog> | undefined {
return _getBufferMap().get(client);
}
function _getBufferMap(): WeakMap<Client, Array<SerializedLog>> {
// The reference to the Client <> LogBuffer map is stored on the carrier to ensure it's always the same
return getGlobalSingleton('clientToLogBufferMap', () => new WeakMap<Client, Array<SerializedLog>>());
}
/**
* Sanitizes serialized log attributes by replacing lone surrogates in both
* keys and string values with U+FFFD.
*/
function sanitizeLogAttributes(attributes: Attributes): Attributes {
const sanitized: Attributes = {};
for (const [key, attr] of Object.entries(attributes)) {
const sanitizedKey = _INTERNAL_removeLoneSurrogates(key);
if (attr.type === 'string') {
sanitized[sanitizedKey] = { ...attr, value: _INTERNAL_removeLoneSurrogates(attr.value as string) };
} else {
sanitized[sanitizedKey] = attr;
}
}
return sanitized;
}
/**
* Replaces unpaired UTF-16 surrogates with U+FFFD (replacement character).
*
* Lone surrogates (U+D800–U+DFFF not part of a valid pair) cause `serde_json`
* on the server to reject the entire log/span batch when they appear in
* JSON-escaped form (e.g. `\uD800`). Replacing them at the SDK level ensures
* only the offending characters are lost instead of the whole payload.
*
* Uses the native `String.prototype.toWellFormed()` when available
* (Node 20+, Chrome 111+, Safari 15.4+, Firefox 119+, Hermes).
* On older runtimes without native support, returns the string as-is.
*/
export function _INTERNAL_removeLoneSurrogates(str: string): string {
// isWellFormed/toWellFormed are ES2024 (not in our TS lib target), so we feature-detect via Object().
const strObj: Record<string, unknown> = Object(str);
if (typeof strObj['isWellFormed'] === 'function') {
return (strObj['isWellFormed'] as () => boolean)() ? str : (strObj['toWellFormed'] as () => string)();
}
return str;
}