@@ -28,6 +28,7 @@ import type {
28
28
SpanContextData ,
29
29
SpanJSON ,
30
30
StartSpanOptions ,
31
+ TraceContext ,
31
32
TransactionEvent ,
32
33
Transport ,
33
34
TransportMakeRequestResponse ,
@@ -44,7 +45,10 @@ import { afterSetupIntegrations } from './integration';
44
45
import { setupIntegration , setupIntegrations } from './integration' ;
45
46
import type { Scope } from './scope' ;
46
47
import { updateSession } from './session' ;
47
- import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext' ;
48
+ import {
49
+ getDynamicSamplingContextFromScope ,
50
+ getDynamicSamplingContextFromSpan ,
51
+ } from './tracing/dynamicSamplingContext' ;
48
52
import { createClientReportEnvelope } from './utils-hoist/clientreport' ;
49
53
import { dsnToString , makeDsn } from './utils-hoist/dsn' ;
50
54
import { addItemToEnvelope , createAttachmentEnvelopeItem } from './utils-hoist/envelope' ;
@@ -57,11 +61,15 @@ import { getPossibleEventMessages } from './utils/eventUtils';
57
61
import { merge } from './utils/merge' ;
58
62
import { parseSampleRate } from './utils/parseSampleRate' ;
59
63
import { prepareEvent } from './utils/prepareEvent' ;
60
- import { showSpanDropWarning } from './utils/spanUtils' ;
64
+ import { showSpanDropWarning , spanToTraceContext } from './utils/spanUtils' ;
61
65
import { convertSpanJsonToTransactionEvent , convertTransactionEventToSpanJson } from './utils/transactionEvent' ;
66
+ import type { Log , SerializedOtelLog } from './types-hoist/log' ;
67
+ import { SEVERITY_TEXT_TO_SEVERITY_NUMBER , createOtelLogEnvelope , logAttributeToSerializedLogAttribute } from './log' ;
68
+ import { _getSpanForScope } from './utils/spanOnScope' ;
62
69
63
70
const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured." ;
64
71
const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release' ;
72
+ const MAX_LOG_BUFFER_SIZE = 100 ;
65
73
66
74
/**
67
75
* Base implementation for all JavaScript SDK clients.
@@ -117,6 +125,8 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
117
125
// eslint-disable-next-line @typescript-eslint/ban-types
118
126
private _hooks : Record < string , Function [ ] > ;
119
127
128
+ private _logsBuffer : Array < SerializedOtelLog > ;
129
+
120
130
/**
121
131
* Initializes this client instance.
122
132
*
@@ -129,6 +139,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
129
139
this . _outcomes = { } ;
130
140
this . _hooks = { } ;
131
141
this . _eventProcessors = [ ] ;
142
+ this . _logsBuffer = [ ] ;
132
143
133
144
if ( options . dsn ) {
134
145
this . _dsn = makeDsn ( options . dsn ) ;
@@ -256,6 +267,58 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
256
267
*/
257
268
public captureCheckIn ?( checkIn : CheckIn , monitorConfig ?: MonitorConfig , scope ?: Scope ) : string ;
258
269
270
+ /**
271
+ * Captures a log event and sends it to Sentry.
272
+ *
273
+ * @param log The log event to capture.
274
+ *
275
+ * @experimental This method will experience breaking changes. This is not yet part of
276
+ * the stable Sentry SDK API and can be changed or removed without warning.
277
+ */
278
+ public captureLog ( { level, message, attributes, severityNumber } : Log , currentScope = getCurrentScope ( ) ) : void {
279
+ const { _experiments, release, environment } = this . getOptions ( ) ;
280
+ if ( ! _experiments ?. enableLogs ) {
281
+ DEBUG_BUILD && logger . warn ( 'logging option not enabled, log will not be captured.' ) ;
282
+ return ;
283
+ }
284
+
285
+ const [ , traceContext ] = _getTraceInfoFromScope ( this , currentScope ) ;
286
+
287
+ const logAttributes = {
288
+ ...attributes ,
289
+ } ;
290
+
291
+ if ( release ) {
292
+ logAttributes . release = release ;
293
+ }
294
+
295
+ if ( environment ) {
296
+ logAttributes . environment = environment ;
297
+ }
298
+
299
+ const span = _getSpanForScope ( currentScope ) ;
300
+ if ( span ) {
301
+ // Add the parent span ID to the log attributes for trace context
302
+ logAttributes [ 'sentry.trace.parent_span_id' ] = span . spanContext ( ) . spanId ;
303
+ }
304
+
305
+ const serializedLog : SerializedOtelLog = {
306
+ severityText : level ,
307
+ body : {
308
+ stringValue : message ,
309
+ } ,
310
+ attributes : Object . entries ( logAttributes ) . map ( ( [ key , value ] ) => logAttributeToSerializedLogAttribute ( key , value ) ) ,
311
+ timeUnixNano : `${ new Date ( ) . getTime ( ) . toString ( ) } 000000` ,
312
+ traceId : traceContext ?. trace_id ,
313
+ severityNumber : severityNumber ?? SEVERITY_TEXT_TO_SEVERITY_NUMBER [ level ] ,
314
+ } ;
315
+
316
+ this . _logsBuffer . push ( serializedLog ) ;
317
+ if ( this . _logsBuffer . length > MAX_LOG_BUFFER_SIZE ) {
318
+ this . _flushLogsBuffer ( ) ;
319
+ }
320
+ }
321
+
259
322
/**
260
323
* Get the current Dsn.
261
324
*/
@@ -295,6 +358,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
295
358
* still events in the queue when the timeout is reached.
296
359
*/
297
360
public flush ( timeout ?: number ) : PromiseLike < boolean > {
361
+ this . _flushLogsBuffer ( ) ;
298
362
const transport = this . _transport ;
299
363
if ( transport ) {
300
364
this . emit ( 'flush' ) ;
@@ -1136,6 +1200,21 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
1136
1200
this . sendEnvelope ( envelope ) ;
1137
1201
}
1138
1202
1203
+ /**
1204
+ * Flushes the logs buffer to Sentry.
1205
+ */
1206
+ protected _flushLogsBuffer ( ) : void {
1207
+ if ( this . _logsBuffer . length === 0 ) {
1208
+ return ;
1209
+ }
1210
+
1211
+ const envelope = createOtelLogEnvelope ( this . _logsBuffer , this . _options . _metadata , this . _options . tunnel , this . _dsn ) ;
1212
+ this . _logsBuffer = [ ] ;
1213
+ // sendEnvelope should not throw
1214
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1215
+ this . sendEnvelope ( envelope ) ;
1216
+ }
1217
+
1139
1218
/**
1140
1219
* Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`.
1141
1220
*/
@@ -1256,3 +1335,20 @@ function isErrorEvent(event: Event): event is ErrorEvent {
1256
1335
function isTransactionEvent ( event : Event ) : event is TransactionEvent {
1257
1336
return event . type === 'transaction' ;
1258
1337
}
1338
+
1339
+ /** Extract trace information from scope */
1340
+ export function _getTraceInfoFromScope (
1341
+ client : Client ,
1342
+ scope : Scope | undefined ,
1343
+ ) : [ dynamicSamplingContext : Partial < DynamicSamplingContext > | undefined , traceContext : TraceContext | undefined ] {
1344
+ if ( ! scope ) {
1345
+ return [ undefined , undefined ] ;
1346
+ }
1347
+
1348
+ const span = _getSpanForScope ( scope ) ;
1349
+ const traceContext = span ? spanToTraceContext ( span ) : getTraceContextFromScope ( scope ) ;
1350
+ const dynamicSamplingContext = span
1351
+ ? getDynamicSamplingContextFromSpan ( span )
1352
+ : getDynamicSamplingContextFromScope ( client , scope ) ;
1353
+ return [ dynamicSamplingContext , traceContext ] ;
1354
+ }
0 commit comments