Skip to content

Commit cda2e4a

Browse files
Add tracingOrigins and urlBlocklist to RN Observe plugin (#107)
1 parent ece4ed3 commit cda2e4a

File tree

22 files changed

+848
-93
lines changed

22 files changed

+848
-93
lines changed

.changeset/sad-emus-turn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@launchdarkly/observability-react-native': patch
3+
---
4+
5+
add tracingOrigins and urlBlocklist and improve span naming

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,3 @@
1717
<!--
1818
Backend - Do we need to consider migrations or backfilling data?
1919
-->
20-
21-
## Does this work require review from our design team?
22-
23-
<!--
24-
Request review from julian-highlight / our design team
25-
-->

sdk/@launchdarkly/observability-react-native/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.1.0] - 2024-01-XX
8+
## [0.1.0] - 2025-07-08
99

1010
### Added
1111
- Initial release of @launchdarkly/observability-react-native

sdk/@launchdarkly/observability-react-native/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"access": "public"
2929
},
3030
"dependencies": {
31+
"@launchdarkly/observability-shared": "workspace:*",
3132
"@opentelemetry/api": "^1.9.0",
3233
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0",
3334
"@opentelemetry/instrumentation": "^0.57.2",

sdk/@launchdarkly/observability-react-native/src/api/Options.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ export interface ReactNativeOptions {
2929
*/
3030
customHeaders?: Record<string, string>
3131

32+
/**
33+
* Specifies where the backend of the app lives. If specified, the SDK will attach tracing headers to outgoing requests whose destination URLs match a substring or regexp from this list, so that backend errors can be linked back to the session.
34+
* If 'true' is specified, all requests to the current domain will be matched.
35+
* @example tracingOrigins: ['localhost', /^\//, 'backend.myapp.com']
36+
*/
37+
tracingOrigins?: boolean | (string | RegExp)[]
38+
39+
/**
40+
* A list of URLs to block from tracing.
41+
* @example urlBlocklist: ['localhost', 'backend.myapp.com']
42+
*/
43+
urlBlocklist?: string[]
44+
3245
/**
3346
* Session timeout in milliseconds.
3447
* @default 30 * 60 * 1000 (30 minutes)

sdk/@launchdarkly/observability-react-native/src/client/InstrumentationManager.ts

Lines changed: 92 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,10 @@ import {
2424
import {
2525
LoggerProvider,
2626
BatchLogRecordProcessor,
27-
LogRecordExporter,
2827
} from '@opentelemetry/sdk-logs'
2928
import {
3029
MeterProvider,
3130
PeriodicExportingMetricReader,
32-
PushMetricExporter,
3331
} from '@opentelemetry/sdk-metrics'
3432
import { Resource } from '@opentelemetry/resources'
3533
import {
@@ -38,64 +36,17 @@ import {
3836
ATTR_EXCEPTION_TYPE,
3937
} from '@opentelemetry/semantic-conventions'
4038
import { SpanStatusCode } from '@opentelemetry/api'
41-
import {
42-
W3CTraceContextPropagator,
43-
W3CBaggagePropagator,
44-
CompositePropagator,
45-
} from '@opentelemetry/core'
39+
import { W3CBaggagePropagator, CompositePropagator } from '@opentelemetry/core'
4640
import { ReactNativeOptions } from '../api/Options'
4741
import { 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

10051
export 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
})

sdk/@launchdarkly/observability-react-native/src/client/ObservabilityClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export class ObservabilityClient {
5757
disableLogs: options.disableLogs ?? false,
5858
disableMetrics: options.disableMetrics ?? false,
5959
disableTraces: options.disableTraces ?? false,
60+
tracingOrigins: options.tracingOrigins ?? false,
61+
urlBlocklist: options.urlBlocklist ?? [],
6062
}
6163
}
6264

@@ -77,10 +79,11 @@ export class ObservabilityClient {
7779
// Old attribute for connecting to LD project. Can be deprecated in the
7880
// future in favor of X-LaunchDarkly-Project header.
7981
'highlight.project_id': this.sdkKey,
82+
'highlight.session_id': sessionAttributes.sessionId,
8083
...this.options.resourceAttributes,
81-
...sessionAttributes,
8284
})
8385

86+
this.instrumentationManager.setSessionManager(this.sessionManager)
8487
this.instrumentationManager.initialize(resource)
8588
this.isInitialized = true
8689

0 commit comments

Comments
 (0)