Skip to content

Commit f5b2a93

Browse files
authored
Merge branch 'main' into agrognetti/O11Y-413
2 parents de39e90 + 6fee471 commit f5b2a93

File tree

67 files changed

+3538
-502
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+3538
-502
lines changed

.changeset/great-comics-ask.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': minor
3+
---
4+
5+
support automated exception reporting for uncaught exceptions, promise rejections and network errors

.changeset/pretty-nails-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'highlight.run': patch
3+
---
4+
5+
update export timeouts for highlight.run

.changeset/shaggy-comics-unite.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': minor
3+
---
4+
5+
add additional error handlers for the react native sdk

.github/workflows/manual-publish-docs.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ on:
88
type: choice
99
options:
1010
- sdk/highlight-run
11+
- 'sdk/@launchdarkly/observability-android'
12+
- 'sdk/@launchdarkly/observability-dotnet'
1113
- 'sdk/@launchdarkly/observability-node'
1214
- 'sdk/@launchdarkly/observability-python'
1315
- 'sdk/@launchdarkly/observability-react-native'
14-
- 'sdk/@launchdarkly/observability-dotnet'
16+
1517
workflow_call:
1618
inputs:
1719
workspace_path:

.release-please-manifest.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"go": "0.2.1",
3-
"sdk/@launchdarkly/observability": "0.4.0",
3+
"sdk/@launchdarkly/observability": "0.4.1",
44
"sdk/@launchdarkly/observability-android": "0.6.0",
5-
"sdk/@launchdarkly/observability-dotnet": "0.1.0",
5+
"sdk/@launchdarkly/observability-dotnet": "0.2.0",
66
"sdk/@launchdarkly/observability-node": "0.3.0",
77
"sdk/@launchdarkly/observability-python": "0.1.1",
8-
"sdk/@launchdarkly/observability-react-native": "0.5.0",
9-
"sdk/@launchdarkly/session-replay": "0.4.0"
8+
"sdk/@launchdarkly/observability-react-native": "0.6.0",
9+
"sdk/@launchdarkly/session-replay": "0.4.1",
10+
"sdk/highlight-run": "9.21.0"
1011
}

e2e/android/app/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ android {
1212
defaultConfig {
1313
applicationId = "com.example.androidobservability"
1414
minSdk = 24
15-
targetSdk = 35
1615
versionCode = 1
1716
versionName = "1.0"
1817

e2e/react-native-otel/app/(tabs)/index.tsx

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export default function HomeScreen() {
5959
component: 'HomeScreen',
6060
},
6161
})
62+
63+
// Test that error reporting is working
64+
console.log(
65+
'Testing error reporting with a message to verify console error handler',
66+
)
6267
}, [])
6368

6469
const handleTestError = () => {
@@ -280,6 +285,184 @@ export default function HomeScreen() {
280285
)
281286
}
282287

288+
// Exception test handlers
289+
const handleUncaughtException = () => {
290+
// This will cause an uncaught exception that should be caught by the global handler
291+
setTimeout(() => {
292+
// Using setTimeout to ensure it's not caught by React's error boundary
293+
const obj: any = null
294+
obj.nonExistentMethod() // Will throw TypeError: Cannot read property 'nonExistentMethod' of null
295+
}, 100)
296+
}
297+
298+
const handleUnhandledPromiseRejection = () => {
299+
// This creates an unhandled Promise rejection
300+
setTimeout(() => {
301+
// Using setTimeout to escape from the current execution context
302+
try {
303+
// Direct rejection using Promise.reject - this should be detected
304+
Promise.reject(new Error('Unhandled Promise Rejection Test'))
305+
306+
// Also try the constructor form
307+
new Promise((resolve, reject) => {
308+
// Explicit rejection without a catch handler
309+
reject(new Error('Unhandled Promise Constructor Rejection'))
310+
})
311+
312+
// Also try throwing in a Promise - this is another common case
313+
new Promise(() => {
314+
throw new Error('Error thrown inside Promise constructor')
315+
})
316+
317+
// Log to confirm execution
318+
console.log(
319+
'Unhandled Promise rejection triggered - check for [LD-O11Y] logs',
320+
)
321+
} catch (e) {
322+
// This shouldn't catch the Promise rejections
323+
console.log(
324+
'This catch should not execute for Promise rejections',
325+
e,
326+
)
327+
}
328+
}, 0)
329+
}
330+
331+
const handleRecursiveError = () => {
332+
// This creates a stack overflow error
333+
// Wrap in setTimeout to bypass React's error boundaries
334+
setTimeout(() => {
335+
const recursiveFunction = (depth: number = 0): number => {
336+
// Add some protection to prevent actually crashing the app
337+
if (depth > 5000) {
338+
throw new Error(
339+
'Maximum recursion depth reached (stack overflow simulation)',
340+
)
341+
}
342+
return recursiveFunction(depth + 1)
343+
}
344+
345+
recursiveFunction()
346+
}, 0)
347+
}
348+
349+
const handleAsyncError = async () => {
350+
// This creates an error in an async function
351+
// Use setTimeout to ensure we're out of the current execution context
352+
setTimeout(() => {
353+
// Direct async/await error
354+
;(async () => {
355+
try {
356+
// First approach: async function throwing directly
357+
const makeAsyncError = async () => {
358+
await new Promise((resolve) => setTimeout(resolve, 200))
359+
console.log('About to throw from async function...')
360+
throw new Error('Async operation failed')
361+
}
362+
363+
// Run without awaiting or catching the error to ensure it becomes unhandled
364+
makeAsyncError()
365+
366+
// Second approach: rejected promise in async function
367+
const makeAsyncRejection = async () => {
368+
await new Promise((resolve) => setTimeout(resolve, 300))
369+
console.log('About to reject from async function...')
370+
await Promise.reject(
371+
new Error('Async promise rejection'),
372+
)
373+
}
374+
375+
// Run without catching
376+
makeAsyncRejection()
377+
378+
// Third approach: await a throwing function
379+
const delayedThrow = () =>
380+
new Promise((_, reject) => {
381+
setTimeout(() => {
382+
console.log(
383+
'About to reject from delayed function...',
384+
)
385+
reject(
386+
new Error(
387+
'Delayed rejection in async context',
388+
),
389+
)
390+
}, 400)
391+
})
392+
393+
// This would be caught if we awaited it, but we don't
394+
delayedThrow()
395+
396+
// Add a log to help track when the errors should occur
397+
console.log(
398+
'Async errors scheduled - watch for [LD-O11Y] logs',
399+
)
400+
} catch (e) {
401+
// This shouldn't execute for the unhandled async errors
402+
console.log(
403+
'This catch should not execute for async errors',
404+
e,
405+
)
406+
}
407+
})()
408+
}, 0)
409+
}
410+
411+
const handleNetworkError = () => {
412+
// This creates a network error by attempting to fetch from a non-existent URL
413+
setTimeout(() => {
414+
console.log('Triggering network error to non-existent domain')
415+
416+
// Multiple network error approaches
417+
418+
// 1. Completely invalid URL - this will cause a synchronous error
419+
try {
420+
// This would normally be caught by React's error boundary, but we want to throw it directly
421+
fetch('invalid://malformed-url').catch((error) => {
422+
// We handle this one so the test can continue
423+
console.log('Caught malformed URL error:', error.message)
424+
LDObserve.recordError(error, {
425+
source: 'network_test',
426+
type: 'malformed_url',
427+
})
428+
})
429+
} catch (e) {
430+
console.log('Synchronous fetch error (malformed URL):', e)
431+
}
432+
433+
// 2. Non-existent domain - this will cause a network error
434+
fetch('https://non-existent-domain-123456789.xyz').then(
435+
(response) => response.json(),
436+
)
437+
// Intentionally no catch handler to make it unhandled
438+
439+
// 3. Timeout error - this will cause a timeout
440+
const controller = new AbortController()
441+
const timeoutId = setTimeout(() => controller.abort(), 100)
442+
443+
fetch('https://httpstat.us/200?sleep=5000', {
444+
signal: controller.signal,
445+
}).then((response) => {
446+
clearTimeout(timeoutId)
447+
return response.json()
448+
})
449+
// Intentionally no catch handler to make it unhandled
450+
451+
// 4. Server error - this should return a non-200 status code
452+
fetch('https://httpstat.us/500').then((response) => {
453+
if (!response.ok) {
454+
throw new Error(
455+
`Server responded with status: ${response.status}`,
456+
)
457+
}
458+
return response.json()
459+
})
460+
// Intentionally no catch handler to make it unhandled
461+
462+
console.log('Network errors triggered - watch for [LD-O11Y] logs')
463+
}, 0)
464+
}
465+
283466
return (
284467
<ParallaxScrollView
285468
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
@@ -344,6 +527,55 @@ export default function HomeScreen() {
344527
Test Span Hierarchy
345528
</ThemedText>
346529
</Pressable>
530+
531+
<ThemedText type="subtitle" style={{ marginTop: 16 }}>
532+
Error Testing
533+
</ThemedText>
534+
535+
<Pressable
536+
style={[styles.button, styles.errorButton]}
537+
onPress={handleUncaughtException}
538+
>
539+
<ThemedText style={styles.buttonText}>
540+
Trigger Uncaught Exception
541+
</ThemedText>
542+
</Pressable>
543+
544+
<Pressable
545+
style={[styles.button, styles.errorButton]}
546+
onPress={handleUnhandledPromiseRejection}
547+
>
548+
<ThemedText style={styles.buttonText}>
549+
Trigger Unhandled Promise Rejection
550+
</ThemedText>
551+
</Pressable>
552+
553+
<Pressable
554+
style={[styles.button, styles.errorButton]}
555+
onPress={handleRecursiveError}
556+
>
557+
<ThemedText style={styles.buttonText}>
558+
Trigger Stack Overflow Error
559+
</ThemedText>
560+
</Pressable>
561+
562+
<Pressable
563+
style={[styles.button, styles.errorButton]}
564+
onPress={handleAsyncError}
565+
>
566+
<ThemedText style={styles.buttonText}>
567+
Trigger Async Error
568+
</ThemedText>
569+
</Pressable>
570+
571+
<Pressable
572+
style={[styles.button, styles.errorButton]}
573+
onPress={handleNetworkError}
574+
>
575+
<ThemedText style={styles.buttonText}>
576+
Trigger Network Error
577+
</ThemedText>
578+
</Pressable>
347579
</ThemedView>
348580

349581
<ThemedView style={styles.stepContainer}>
@@ -407,6 +639,9 @@ const styles = StyleSheet.create({
407639
marginVertical: 4,
408640
alignItems: 'center',
409641
},
642+
errorButton: {
643+
backgroundColor: '#FF3B30', // Red color for error buttons
644+
},
410645
buttonText: {
411646
color: 'white',
412647
fontWeight: 'bold',

0 commit comments

Comments
 (0)