11import * as OpenTelemetry from '@opentelemetry/api' ;
2+ import { Resource } from '@opentelemetry/resources' ;
23import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base' ;
34import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' ;
5+ import { SemanticAttributes , SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' ;
46import { 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' ;
69
710import { SentrySpanProcessor } from '../src/spanprocessor' ;
811
@@ -22,7 +25,11 @@ describe('SentrySpanProcessor', () => {
2225 makeMain ( hub ) ;
2326
2427 spanProcessor = new SentrySpanProcessor ( ) ;
25- provider = new NodeTracerProvider ( ) ;
28+ provider = new NodeTracerProvider ( {
29+ resource : new Resource ( {
30+ [ SemanticResourceAttributes . SERVICE_NAME ] : 'test-service' ,
31+ } ) ,
32+ } ) ;
2633 provider . addSpanProcessor ( spanProcessor ) ;
2734 provider . register ( ) ;
2835 } ) ;
@@ -36,6 +43,27 @@ describe('SentrySpanProcessor', () => {
3643 return spanProcessor . _map . get ( otelSpan . spanContext ( ) . spanId ) ;
3744 }
3845
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+
3967 it ( 'creates a transaction' , async ( ) => {
4068 const startTime = otelNumberToHrtime ( new Date ( ) . valueOf ( ) ) ;
4169
@@ -125,6 +153,181 @@ describe('SentrySpanProcessor', () => {
125153 parentOtelSpan . end ( ) ;
126154 } ) ;
127155 } ) ;
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+ ) ;
128331} ) ;
129332
130333// OTEL expects a custom date format
0 commit comments