@@ -24,12 +24,10 @@ import {
2424import {
2525 LoggerProvider ,
2626 BatchLogRecordProcessor ,
27- LogRecordExporter ,
2827} from '@opentelemetry/sdk-logs'
2928import {
3029 MeterProvider ,
3130 PeriodicExportingMetricReader ,
32- PushMetricExporter ,
3331} from '@opentelemetry/sdk-metrics'
3432import { Resource } from '@opentelemetry/resources'
3533import {
@@ -38,64 +36,17 @@ import {
3836 ATTR_EXCEPTION_TYPE ,
3937} from '@opentelemetry/semantic-conventions'
4038import { SpanStatusCode } from '@opentelemetry/api'
41- import {
42- W3CTraceContextPropagator ,
43- W3CBaggagePropagator ,
44- CompositePropagator ,
45- } from '@opentelemetry/core'
39+ import { W3CBaggagePropagator , CompositePropagator } from '@opentelemetry/core'
4640import { ReactNativeOptions } from '../api/Options'
4741import { Metric } from '../api/Metric'
48-
49- export class CustomBatchSpanProcessor extends BatchSpanProcessor {
50- private recentHttpSpans = new Map < string , number > ( )
51- private readonly DEDUP_WINDOW_MS = 1000
52-
53- constructor ( exporter : SpanExporter , options ?: BufferConfig ) {
54- super ( exporter , options )
55- }
56-
57- onEnd ( span : ReadableSpan ) : void {
58- if ( this . isHttpSpan ( span ) ) {
59- const spanKey = this . generateHttpSpanKey ( span )
60- const now = Date . now ( )
61-
62- this . cleanupOldHttpSpans ( now )
63-
64- if ( this . recentHttpSpans . has ( spanKey ) ) {
65- return // duplicate - skip
66- }
67-
68- this . recentHttpSpans . set ( spanKey , now )
69- super . onEnd ( span )
70-
71- return
72- }
73-
74- super . onEnd ( span )
75- }
76-
77- private isHttpSpan ( span : ReadableSpan ) : boolean {
78- const url = span . attributes [ 'http.url' ]
79- const method = span . attributes [ 'http.method' ]
80- return Boolean ( url && method )
81- }
82-
83- private generateHttpSpanKey ( span : ReadableSpan ) : string {
84- const url = span . attributes [ 'http.url' ] as string
85- const method = span . attributes [ 'http.method' ] as string
86- const startTime = Math . floor ( span . startTime [ 0 ] )
87-
88- return `${ method } :${ url } :${ startTime } `
89- }
90-
91- private cleanupOldHttpSpans ( now : number ) : void {
92- for ( const [ key , timestamp ] of this . recentHttpSpans . entries ( ) ) {
93- if ( now - timestamp > this . DEDUP_WINDOW_MS ) {
94- this . recentHttpSpans . delete ( key )
95- }
96- }
97- }
98- }
42+ import { SessionManager } from './SessionManager'
43+ import {
44+ CustomTraceContextPropagator ,
45+ getCorsUrlsPattern ,
46+ getSpanName ,
47+ } from '@launchdarkly/observability-shared'
48+ import { DeduplicatingExporter } from '../otel/DeduplicatingExporter'
49+ import { CustomBatchSpanProcessor } from '../otel/CustomBatchSpanProcessor'
9950
10051export class InstrumentationManager {
10152 private traceProvider ?: WebTracerProvider
@@ -105,11 +56,9 @@ export class InstrumentationManager {
10556 private serviceName : string
10657 private resource : Resource = new Resource ( { } )
10758 private headers : Record < string , string > = { }
108- private traceExporter ?: SpanExporter
109- private logExporter ?: LogRecordExporter
110- private metricExporter ?: PushMetricExporter
59+ private sessionManager ?: SessionManager
11160
112- constructor ( private options : ReactNativeOptions ) {
61+ constructor ( private options : Required < ReactNativeOptions > ) {
11362 this . serviceName =
11463 this . options . serviceName ??
11564 'launchdarkly-observability-react-native'
@@ -135,31 +84,46 @@ export class InstrumentationManager {
13584 }
13685 }
13786
87+ public setSessionManager ( sessionManager : SessionManager ) {
88+ this . sessionManager = sessionManager
89+ }
90+
13891 private initializeTracing ( ) {
13992 if ( this . options . disableTraces ) return
14093
14194 const compositePropagator = new CompositePropagator ( {
14295 propagators : [
143- new W3CTraceContextPropagator ( ) ,
14496 new W3CBaggagePropagator ( ) ,
97+ new CustomTraceContextPropagator ( {
98+ internalEndpoints : [
99+ `${ this . options . otlpEndpoint } /v1/traces` ,
100+ `${ this . options . otlpEndpoint } /v1/logs` ,
101+ `${ this . options . otlpEndpoint } /v1/metrics` ,
102+ ] ,
103+ tracingOrigins : this . options . tracingOrigins ,
104+ urlBlocklist : this . options . urlBlocklist ,
105+ } ) ,
145106 ] ,
146107 } )
147108
148109 propagation . setGlobalPropagator ( compositePropagator )
149110
150- const exporter =
151- this . traceExporter ??
152- new OTLPTraceExporter ( {
153- url : `${ this . options . otlpEndpoint } /v1/traces` ,
154- headers : this . headers ,
155- } )
111+ const otlpExporter = new OTLPTraceExporter ( {
112+ url : `${ this . options . otlpEndpoint } /v1/traces` ,
113+ headers : this . headers ,
114+ } )
115+ const exporter = new DeduplicatingExporter (
116+ otlpExporter ,
117+ this . options . debug ,
118+ )
156119
157120 const processors : SpanProcessor [ ] = [
158121 new CustomBatchSpanProcessor ( exporter , {
159122 maxQueueSize : 100 ,
160123 scheduledDelayMillis : 500 ,
161124 exportTimeoutMillis : 5000 ,
162125 maxExportBatchSize : 10 ,
126+ debug : this . options . debug ,
163127 } ) ,
164128 ]
165129
@@ -171,15 +135,52 @@ export class InstrumentationManager {
171135 this . traceProvider . register ( )
172136 trace . setGlobalTracerProvider ( this . traceProvider )
173137
138+ const corsPattern = getCorsUrlsPattern ( this . options . tracingOrigins )
139+
174140 registerInstrumentations ( {
175141 instrumentations : [
176142 new FetchInstrumentation ( {
177- // TODO: Verify this works the same as the web implementation.
178- // Look at getCorsUrlsPattern. Take into account tracingOrigins.
179- propagateTraceHeaderCorsUrls : / .* / ,
143+ applyCustomAttributesOnSpan : ( span , request ) => {
144+ if ( ! ( span as any ) . attributes ) {
145+ return
146+ }
147+ const readableSpan = span as unknown as ReadableSpan
148+
149+ const url = readableSpan . attributes [
150+ 'http.url'
151+ ] as string
152+ const method = request . method ?? 'GET'
153+
154+ span . updateName ( getSpanName ( url , method , request . body ) )
155+ } ,
156+ propagateTraceHeaderCorsUrls : corsPattern ,
180157 } ) ,
181158 new XMLHttpRequestInstrumentation ( {
182- propagateTraceHeaderCorsUrls : / .* / ,
159+ applyCustomAttributesOnSpan : ( span , xhr ) => {
160+ if ( ! ( span as any ) . attributes ) {
161+ return
162+ }
163+ const readableSpan = span as unknown as ReadableSpan
164+
165+ try {
166+ const url = readableSpan . attributes [
167+ 'http.url'
168+ ] as string
169+ const method = readableSpan . attributes [
170+ 'http.method'
171+ ] as string
172+ let responseText : string | undefined
173+ if ( [ '' , 'text' ] . includes ( xhr . responseType ) ) {
174+ responseText = xhr . responseText
175+ }
176+ span . updateName (
177+ getSpanName ( url , method , responseText ) ,
178+ )
179+ } catch ( e ) {
180+ console . error ( 'Failed to update span name:' , e )
181+ }
182+ } ,
183+ propagateTraceHeaderCorsUrls : corsPattern ,
183184 } ) ,
184185 ] ,
185186 } )
@@ -245,14 +246,17 @@ export class InstrumentationManager {
245246 try {
246247 const activeSpan = options ?. span || trace . getActiveSpan ( )
247248 const span = activeSpan ?? this . getTracer ( ) . startSpan ( 'error' )
249+ const sessionId = this . sessionManager ?. getSessionInfo ( ) . sessionId
248250
249251 span . recordException ( error )
250252 span . setAttribute ( ATTR_EXCEPTION_MESSAGE , error . message )
251- span . setAttribute (
252- ATTR_EXCEPTION_STACKTRACE ,
253- error . stack ?? 'No stack trace' ,
254- )
255253 span . setAttribute ( ATTR_EXCEPTION_TYPE , error . name ?? 'No name' )
254+ if ( error . stack ) {
255+ span . setAttribute ( ATTR_EXCEPTION_STACKTRACE , error . stack )
256+ }
257+ if ( sessionId ) {
258+ span . setAttribute ( 'highlight.session_id' , sessionId )
259+ }
256260 span . setStatus ( { code : SpanStatusCode . ERROR } )
257261
258262 if ( attributes ) {
@@ -268,6 +272,11 @@ export class InstrumentationManager {
268272 'exception.type' : error . name ,
269273 'exception.message' : error . message ,
270274 'exception.stacktrace' : error . stack ,
275+ ...( sessionId
276+ ? {
277+ [ 'highlight.session_id' ] : sessionId ,
278+ }
279+ : { } ) ,
271280 } )
272281 } catch ( e ) {
273282 console . error ( 'Failed to record error:' , e )
@@ -320,6 +329,7 @@ export class InstrumentationManager {
320329 ) : void {
321330 try {
322331 const logger = this . getLogger ( )
332+ const sessionId = this . sessionManager ?. getSessionInfo ( ) . sessionId
323333
324334 logger . emit ( {
325335 severityText : level . toUpperCase ( ) ,
@@ -330,6 +340,11 @@ export class InstrumentationManager {
330340 attributes : {
331341 ...attributes ,
332342 'log.source' : 'react-native-plugin' ,
343+ ...( sessionId
344+ ? {
345+ [ 'highlight.session_id' ] : sessionId ,
346+ }
347+ : { } ) ,
333348 } ,
334349 timestamp : Date . now ( ) ,
335350 } )
0 commit comments