1
1
import * as OpenTelemetry from '@opentelemetry/api' ;
2
+ import { Resource } from '@opentelemetry/resources' ;
2
3
import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base' ;
3
4
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' ;
5
+ import { SemanticAttributes , SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' ;
4
6
import { Hub , makeMain } from '@sentry/core' ;
5
- import { addExtensionMethods , Span as SentrySpan , Transaction } from '@sentry/tracing' ;
7
+ import { addExtensionMethods , Span as SentrySpan , SpanStatusType , Transaction } from '@sentry/tracing' ;
8
+ import { Contexts , Scope } from '@sentry/types' ;
6
9
7
10
import { SentrySpanProcessor } from '../src/spanprocessor' ;
8
11
@@ -22,7 +25,11 @@ describe('SentrySpanProcessor', () => {
22
25
makeMain ( hub ) ;
23
26
24
27
spanProcessor = new SentrySpanProcessor ( ) ;
25
- provider = new NodeTracerProvider ( ) ;
28
+ provider = new NodeTracerProvider ( {
29
+ resource : new Resource ( {
30
+ [ SemanticResourceAttributes . SERVICE_NAME ] : 'test-service' ,
31
+ } ) ,
32
+ } ) ;
26
33
provider . addSpanProcessor ( spanProcessor ) ;
27
34
provider . register ( ) ;
28
35
} ) ;
@@ -36,6 +43,27 @@ describe('SentrySpanProcessor', () => {
36
43
return spanProcessor . _map . get ( otelSpan . spanContext ( ) . spanId ) ;
37
44
}
38
45
46
+ function getContext ( transaction : Transaction ) {
47
+ const transactionWithContext = transaction as unknown as Transaction & { _contexts : Contexts } ;
48
+ return transactionWithContext . _contexts ;
49
+ }
50
+
51
+ // monkey-patch finish to store the context at finish time
52
+ function monkeyPatchTransactionFinish ( transaction : Transaction ) {
53
+ const monkeyPatchedTransaction = transaction as Transaction & { _contexts : Contexts } ;
54
+
55
+ // eslint-disable-next-line @typescript-eslint/unbound-method
56
+ const originalFinish = monkeyPatchedTransaction . finish ;
57
+ monkeyPatchedTransaction . _contexts = { } ;
58
+ monkeyPatchedTransaction . finish = function ( endTimestamp ?: number | undefined ) {
59
+ monkeyPatchedTransaction . _contexts = (
60
+ transaction . _hub . getScope ( ) as unknown as Scope & { _contexts : Contexts }
61
+ ) . _contexts ;
62
+
63
+ return originalFinish . apply ( monkeyPatchedTransaction , [ endTimestamp ] ) ;
64
+ } ;
65
+ }
66
+
39
67
it ( 'creates a transaction' , async ( ) => {
40
68
const startTime = otelNumberToHrtime ( new Date ( ) . valueOf ( ) ) ;
41
69
@@ -125,6 +153,181 @@ describe('SentrySpanProcessor', () => {
125
153
parentOtelSpan . end ( ) ;
126
154
} ) ;
127
155
} ) ;
156
+
157
+ it ( 'sets context for transaction' , async ( ) => {
158
+ const otelSpan = provider . getTracer ( 'default' ) . startSpan ( 'GET /users' ) ;
159
+
160
+ const transaction = getSpanForOtelSpan ( otelSpan ) as Transaction ;
161
+ monkeyPatchTransactionFinish ( transaction ) ;
162
+
163
+ // context is only set after end
164
+ expect ( getContext ( transaction ) ) . toEqual ( { } ) ;
165
+
166
+ otelSpan . end ( ) ;
167
+
168
+ expect ( getContext ( transaction ) ) . toEqual ( {
169
+ otel : {
170
+ attributes : { } ,
171
+ resource : {
172
+ 'service.name' : 'test-service' ,
173
+ 'telemetry.sdk.language' : 'nodejs' ,
174
+ 'telemetry.sdk.name' : 'opentelemetry' ,
175
+ 'telemetry.sdk.version' : '1.7.0' ,
176
+ } ,
177
+ } ,
178
+ } ) ;
179
+
180
+ // Start new transaction
181
+ const otelSpan2 = provider . getTracer ( 'default' ) . startSpan ( 'GET /companies' ) ;
182
+
183
+ const transaction2 = getSpanForOtelSpan ( otelSpan2 ) as Transaction ;
184
+ monkeyPatchTransactionFinish ( transaction2 ) ;
185
+
186
+ expect ( getContext ( transaction2 ) ) . toEqual ( { } ) ;
187
+
188
+ otelSpan2 . setAttribute ( 'test-attribute' , 'test-value' ) ;
189
+
190
+ otelSpan2 . end ( ) ;
191
+
192
+ expect ( getContext ( transaction2 ) ) . toEqual ( {
193
+ otel : {
194
+ attributes : {
195
+ 'test-attribute' : 'test-value' ,
196
+ } ,
197
+ resource : {
198
+ 'service.name' : 'test-service' ,
199
+ 'telemetry.sdk.language' : 'nodejs' ,
200
+ 'telemetry.sdk.name' : 'opentelemetry' ,
201
+ 'telemetry.sdk.version' : '1.7.0' ,
202
+ } ,
203
+ } ,
204
+ } ) ;
205
+ } ) ;
206
+
207
+ it ( 'sets data for span' , async ( ) => {
208
+ const tracer = provider . getTracer ( 'default' ) ;
209
+
210
+ tracer . startActiveSpan ( 'GET /users' , parentOtelSpan => {
211
+ tracer . startActiveSpan ( 'SELECT * FROM users;' , child => {
212
+ child . setAttribute ( 'test-attribute' , 'test-value' ) ;
213
+ child . setAttribute ( 'test-attribute-2' , [ 1 , 2 , 3 ] ) ;
214
+ child . setAttribute ( 'test-attribute-3' , 0 ) ;
215
+ child . setAttribute ( 'test-attribute-4' , false ) ;
216
+
217
+ const sentrySpan = getSpanForOtelSpan ( child ) ;
218
+
219
+ expect ( sentrySpan ?. data ) . toEqual ( { } ) ;
220
+
221
+ child . end ( ) ;
222
+
223
+ expect ( sentrySpan ?. data ) . toEqual ( {
224
+ 'otel.kind' : 0 ,
225
+ 'test-attribute' : 'test-value' ,
226
+ 'test-attribute-2' : [ 1 , 2 , 3 ] ,
227
+ 'test-attribute-3' : 0 ,
228
+ 'test-attribute-4' : false ,
229
+ } ) ;
230
+ } ) ;
231
+
232
+ parentOtelSpan . end ( ) ;
233
+ } ) ;
234
+ } ) ;
235
+
236
+ it ( 'sets status for transaction' , async ( ) => {
237
+ const otelSpan = provider . getTracer ( 'default' ) . startSpan ( 'GET /users' ) ;
238
+
239
+ const transaction = getSpanForOtelSpan ( otelSpan ) as Transaction ;
240
+
241
+ // status is only set after end
242
+ expect ( transaction ?. status ) . toBe ( undefined ) ;
243
+
244
+ otelSpan . end ( ) ;
245
+
246
+ expect ( transaction ?. status ) . toBe ( 'ok' ) ;
247
+ } ) ;
248
+
249
+ it ( 'sets status for span' , async ( ) => {
250
+ const tracer = provider . getTracer ( 'default' ) ;
251
+
252
+ tracer . startActiveSpan ( 'GET /users' , parentOtelSpan => {
253
+ tracer . startActiveSpan ( 'SELECT * FROM users;' , child => {
254
+ const sentrySpan = getSpanForOtelSpan ( child ) ;
255
+
256
+ expect ( sentrySpan ?. status ) . toBe ( undefined ) ;
257
+
258
+ child . end ( ) ;
259
+
260
+ expect ( sentrySpan ?. status ) . toBe ( 'ok' ) ;
261
+
262
+ parentOtelSpan . end ( ) ;
263
+ } ) ;
264
+ } ) ;
265
+ } ) ;
266
+
267
+ const statusTestTable : [ number , undefined | string , undefined | string , SpanStatusType ] [ ] = [
268
+ [ - 1 , undefined , undefined , 'unknown_error' ] ,
269
+ [ 3 , undefined , undefined , 'unknown_error' ] ,
270
+ [ 0 , undefined , undefined , 'ok' ] ,
271
+ [ 1 , undefined , undefined , 'ok' ] ,
272
+ [ 2 , undefined , undefined , 'unknown_error' ] ,
273
+
274
+ // http codes
275
+ [ 2 , '400' , undefined , 'failed_precondition' ] ,
276
+ [ 2 , '401' , undefined , 'unauthenticated' ] ,
277
+ [ 2 , '403' , undefined , 'permission_denied' ] ,
278
+ [ 2 , '404' , undefined , 'not_found' ] ,
279
+ [ 2 , '409' , undefined , 'aborted' ] ,
280
+ [ 2 , '429' , undefined , 'resource_exhausted' ] ,
281
+ [ 2 , '499' , undefined , 'cancelled' ] ,
282
+ [ 2 , '500' , undefined , 'internal_error' ] ,
283
+ [ 2 , '501' , undefined , 'unimplemented' ] ,
284
+ [ 2 , '503' , undefined , 'unavailable' ] ,
285
+ [ 2 , '504' , undefined , 'deadline_exceeded' ] ,
286
+ [ 2 , '999' , undefined , 'unknown_error' ] ,
287
+
288
+ // grpc codes
289
+ [ 2 , undefined , '1' , 'cancelled' ] ,
290
+ [ 2 , undefined , '2' , 'unknown_error' ] ,
291
+ [ 2 , undefined , '3' , 'invalid_argument' ] ,
292
+ [ 2 , undefined , '4' , 'deadline_exceeded' ] ,
293
+ [ 2 , undefined , '5' , 'not_found' ] ,
294
+ [ 2 , undefined , '6' , 'already_exists' ] ,
295
+ [ 2 , undefined , '7' , 'permission_denied' ] ,
296
+ [ 2 , undefined , '8' , 'resource_exhausted' ] ,
297
+ [ 2 , undefined , '9' , 'failed_precondition' ] ,
298
+ [ 2 , undefined , '10' , 'aborted' ] ,
299
+ [ 2 , undefined , '11' , 'out_of_range' ] ,
300
+ [ 2 , undefined , '12' , 'unimplemented' ] ,
301
+ [ 2 , undefined , '13' , 'internal_error' ] ,
302
+ [ 2 , undefined , '14' , 'unavailable' ] ,
303
+ [ 2 , undefined , '15' , 'data_loss' ] ,
304
+ [ 2 , undefined , '16' , 'unauthenticated' ] ,
305
+ [ 2 , undefined , '999' , 'unknown_error' ] ,
306
+
307
+ // http takes precedence over grpc
308
+ [ 2 , '400' , '2' , 'failed_precondition' ] ,
309
+ ] ;
310
+
311
+ it . each ( statusTestTable ) (
312
+ 'correctly converts otel span status to sentry status with otelStatus=%i, httpCode=%s, grpcCode=%s' ,
313
+ ( otelStatus , httpCode , grpcCode , expected ) => {
314
+ const otelSpan = provider . getTracer ( 'default' ) . startSpan ( 'GET /users' ) ;
315
+ const transaction = getSpanForOtelSpan ( otelSpan ) as Transaction ;
316
+
317
+ otelSpan . setStatus ( { code : otelStatus } ) ;
318
+
319
+ if ( httpCode ) {
320
+ otelSpan . setAttribute ( SemanticAttributes . HTTP_STATUS_CODE , httpCode ) ;
321
+ }
322
+
323
+ if ( grpcCode ) {
324
+ otelSpan . setAttribute ( SemanticAttributes . RPC_GRPC_STATUS_CODE , grpcCode ) ;
325
+ }
326
+
327
+ otelSpan . end ( ) ;
328
+ expect ( transaction ?. status ) . toBe ( expected ) ;
329
+ } ,
330
+ ) ;
128
331
} ) ;
129
332
130
333
// OTEL expects a custom date format
0 commit comments