Skip to content

Commit 840c8c6

Browse files
committed
Merge branch 'main' into andrey/tile-compression
* main: chore: release main (#396) fix: Android span e2e tests (#397) fix: improve network response capture (#379) feat: initial upload of mobile-dotnet (#393) chore: release main (#394) fix: Fix event sorting in session replay export (#392) fix: Touch move event buffering using wrong clock and mismatched constants (#391)
2 parents 92b59c4 + 8cbd73f commit 840c8c6

107 files changed

Lines changed: 5784 additions & 122 deletions

File tree

Some content is hidden

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

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,12 @@ sdk/highlight-wordpress/highlight-io/highlight.js
7070
# Typedoc docs
7171
sdk/highlight-run/docs
7272
sdk/@launchdarkly/observability-react-native/docs
73+
74+
# .NET mobile SDK build artifacts
75+
sdk/@launchdarkly/mobile-dotnet/**/bin/
76+
sdk/@launchdarkly/mobile-dotnet/**/obj/
77+
sdk/@launchdarkly/mobile-dotnet/**/.gradle/
78+
sdk/@launchdarkly/mobile-dotnet/**/*.Local.json
79+
sdk/@launchdarkly/mobile-dotnet/**/*.csproj.user
80+
sdk/@launchdarkly/mobile-dotnet/**/*.sln
81+
sdk/@launchdarkly/mobile-dotnet/**/nupkgs/

.release-please-manifest.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"go": "1.0.0",
3-
"sdk/@launchdarkly/observability": "1.0.2",
4-
"sdk/@launchdarkly/observability-android": "0.26.0",
3+
"sdk/@launchdarkly/observability": "1.0.3",
4+
"sdk/@launchdarkly/observability-android": "0.26.1",
55
"sdk/@launchdarkly/observability-dotnet": "1.0.0",
66
"sdk/@launchdarkly/observability-node": "1.0.1",
77
"sdk/@launchdarkly/observability-python": "1.1.0",
88
"sdk/@launchdarkly/observability-react-native": "0.7.0",
99
"sdk/@launchdarkly/react-native-ld-session-replay": "0.2.0",
10-
"sdk/@launchdarkly/session-replay": "1.0.2",
11-
"sdk/highlight-run": "9.27.0"
10+
"sdk/@launchdarkly/session-replay": "1.0.3",
11+
"sdk/highlight-run": "9.27.1"
1212
}

e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ open class BaseApplication : Application() {
9191
LDClient.init(this@BaseApplication, ldConfig, context)
9292
telemetryInspector = observabilityPlugin.getTelemetryInspector()
9393

94-
flagEvaluation()
94+
if (testUrl == null) {
95+
// intervenes in E2E tests by trigger spans
96+
flagEvaluation()
97+
}
9598

9699
LDReplay.start()
97100
}

e2e/react-router/src/routes/http-test.tsx

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,281 @@ export default function HttpTest() {
517517
/>
518518
</TestSection>
519519

520+
<TestSection
521+
title="Response Body Capture Tests"
522+
description="Test that http.response.body is captured on fetch spans. Previously, the async body read raced with span.end() causing the body attribute to be silently dropped."
523+
>
524+
<TestButton
525+
title="GET Response Body"
526+
description="http.response.body should contain JSON"
527+
onClick={async () => {
528+
try {
529+
const response = await fetch(
530+
'https://jsonplaceholder.typicode.com/posts/1?test=response-body-get',
531+
)
532+
const data = await response.json()
533+
console.log('GET response body capture test:', data)
534+
} catch (e) {
535+
console.error('Request error:', e)
536+
}
537+
}}
538+
/>
539+
540+
<TestButton
541+
title="Large Response Body"
542+
description="Body capture with ~5KB response"
543+
onClick={async () => {
544+
try {
545+
const response = await fetch(
546+
'https://jsonplaceholder.typicode.com/posts?test=response-body-large&_limit=10',
547+
)
548+
const data = await response.json()
549+
console.log(
550+
'Large response body capture test:',
551+
`${data.length} items, ~${JSON.stringify(data).length} bytes`,
552+
)
553+
} catch (e) {
554+
console.error('Request error:', e)
555+
}
556+
}}
557+
/>
558+
559+
<TestButton
560+
title="POST Response Body"
561+
description="Both request and response bodies captured"
562+
onClick={async () => {
563+
try {
564+
const response = await fetch(
565+
'https://jsonplaceholder.typicode.com/posts?test=response-body-post',
566+
{
567+
method: 'POST',
568+
headers: {
569+
'Content-Type': 'application/json',
570+
},
571+
body: JSON.stringify({
572+
title: 'Test',
573+
body: 'Verify both request and response bodies are on the span',
574+
userId: 1,
575+
}),
576+
},
577+
)
578+
const data = await response.json()
579+
console.log(
580+
'POST response body capture test:',
581+
data,
582+
)
583+
} catch (e) {
584+
console.error('Request error:', e)
585+
}
586+
}}
587+
/>
588+
589+
<TestButton
590+
title="Concurrent Response Bodies"
591+
description="5 parallel requests, all should have bodies"
592+
onClick={async () => {
593+
try {
594+
const promises = []
595+
for (let i = 1; i <= 5; i++) {
596+
promises.push(
597+
fetch(
598+
`https://jsonplaceholder.typicode.com/posts/${i}?test=response-body-concurrent-${i}`,
599+
),
600+
)
601+
}
602+
const responses = await Promise.all(promises)
603+
const data = await Promise.all(
604+
responses.map((r) => r.json()),
605+
)
606+
console.log(
607+
'Concurrent response body capture test:',
608+
`${data.length} responses with bodies`,
609+
)
610+
} catch (e) {
611+
console.error('Request error:', e)
612+
}
613+
}}
614+
/>
615+
</TestSection>
616+
617+
<TestSection
618+
title="Memory Pressure Tests"
619+
description="Stress test the pendingResponseAttributes map and response body cloning under high concurrency. Use browser DevTools Memory tab to observe heap impact."
620+
>
621+
<TestButton
622+
title="50 Concurrent Requests"
623+
description="Flood pendingResponseAttributes map"
624+
onClick={async () => {
625+
const before = (
626+
performance as unknown as {
627+
memory?: { usedJSHeapSize: number }
628+
}
629+
).memory?.usedJSHeapSize
630+
try {
631+
const promises = []
632+
for (let i = 1; i <= 50; i++) {
633+
promises.push(
634+
fetch(
635+
`https://jsonplaceholder.typicode.com/posts/${(i % 100) + 1}?test=memory-concurrent-${i}`,
636+
),
637+
)
638+
}
639+
const responses = await Promise.all(promises)
640+
await Promise.all(responses.map((r) => r.json()))
641+
const after = (
642+
performance as unknown as {
643+
memory?: { usedJSHeapSize: number }
644+
}
645+
).memory?.usedJSHeapSize
646+
if (before && after) {
647+
console.log(
648+
`Memory: 50 concurrent requests — heap delta: ${((after - before) / 1024).toFixed(0)}KB (${(before / 1024 / 1024).toFixed(1)}MB → ${(after / 1024 / 1024).toFixed(1)}MB)`,
649+
)
650+
} else {
651+
console.log(
652+
'Memory: 50 concurrent requests completed (enable chrome://flags/#enable-precise-memory-info for heap stats)',
653+
)
654+
}
655+
} catch (e) {
656+
console.error('Memory test error:', e)
657+
}
658+
}}
659+
/>
660+
661+
<TestButton
662+
title="Large Bodies x20"
663+
description="20 concurrent requests with ~30KB bodies"
664+
onClick={async () => {
665+
const before = (
666+
performance as unknown as {
667+
memory?: { usedJSHeapSize: number }
668+
}
669+
).memory?.usedJSHeapSize
670+
try {
671+
const promises = []
672+
for (let i = 0; i < 20; i++) {
673+
// /comments returns ~30KB
674+
promises.push(
675+
fetch(
676+
`https://jsonplaceholder.typicode.com/comments?test=memory-large-${i}`,
677+
),
678+
)
679+
}
680+
const responses = await Promise.all(promises)
681+
const bodies = await Promise.all(
682+
responses.map((r) => r.json()),
683+
)
684+
const totalSize = bodies.reduce(
685+
(sum, b) => sum + JSON.stringify(b).length,
686+
0,
687+
)
688+
const after = (
689+
performance as unknown as {
690+
memory?: { usedJSHeapSize: number }
691+
}
692+
).memory?.usedJSHeapSize
693+
if (before && after) {
694+
console.log(
695+
`Memory: 20 large responses (~${(totalSize / 1024).toFixed(0)}KB total) — heap delta: ${((after - before) / 1024).toFixed(0)}KB (${(before / 1024 / 1024).toFixed(1)}MB → ${(after / 1024 / 1024).toFixed(1)}MB)`,
696+
)
697+
} else {
698+
console.log(
699+
`Memory: 20 large responses (~${(totalSize / 1024).toFixed(0)}KB total) completed`,
700+
)
701+
}
702+
} catch (e) {
703+
console.error('Memory test error:', e)
704+
}
705+
}}
706+
/>
707+
708+
<TestButton
709+
title="Sustained Rapid Fire"
710+
description="10 batches of 10, back-to-back"
711+
onClick={async () => {
712+
const before = (
713+
performance as unknown as {
714+
memory?: { usedJSHeapSize: number }
715+
}
716+
).memory?.usedJSHeapSize
717+
try {
718+
for (let batch = 0; batch < 10; batch++) {
719+
const promises = []
720+
for (let i = 0; i < 10; i++) {
721+
promises.push(
722+
fetch(
723+
`https://jsonplaceholder.typicode.com/posts/${(i % 100) + 1}?test=memory-rapid-b${batch}-${i}`,
724+
),
725+
)
726+
}
727+
const responses = await Promise.all(promises)
728+
await Promise.all(
729+
responses.map((r) => r.json()),
730+
)
731+
}
732+
const after = (
733+
performance as unknown as {
734+
memory?: { usedJSHeapSize: number }
735+
}
736+
).memory?.usedJSHeapSize
737+
if (before && after) {
738+
console.log(
739+
`Memory: 10x10 sustained load — heap delta: ${((after - before) / 1024).toFixed(0)}KB (${(before / 1024 / 1024).toFixed(1)}MB → ${(after / 1024 / 1024).toFixed(1)}MB)`,
740+
)
741+
} else {
742+
console.log(
743+
'Memory: 10x10 sustained load completed',
744+
)
745+
}
746+
} catch (e) {
747+
console.error('Memory test error:', e)
748+
}
749+
}}
750+
/>
751+
752+
<TestButton
753+
title="Fire-and-Forget (no await body)"
754+
description="50 requests where body is never read by app"
755+
onClick={async () => {
756+
const before = (
757+
performance as unknown as {
758+
memory?: { usedJSHeapSize: number }
759+
}
760+
).memory?.usedJSHeapSize
761+
try {
762+
const promises = []
763+
for (let i = 0; i < 50; i++) {
764+
promises.push(
765+
fetch(
766+
`https://jsonplaceholder.typicode.com/posts/${(i % 100) + 1}?test=memory-fire-forget-${i}`,
767+
),
768+
)
769+
}
770+
// Only wait for responses, don't read bodies.
771+
// The SDK still clones + reads each body internally,
772+
// so this tests whether un-consumed bodies leak.
773+
await Promise.all(promises)
774+
const after = (
775+
performance as unknown as {
776+
memory?: { usedJSHeapSize: number }
777+
}
778+
).memory?.usedJSHeapSize
779+
if (before && after) {
780+
console.log(
781+
`Memory: 50 fire-and-forget — heap delta: ${((after - before) / 1024).toFixed(0)}KB (${(before / 1024 / 1024).toFixed(1)}MB → ${(after / 1024 / 1024).toFixed(1)}MB)`,
782+
)
783+
} else {
784+
console.log(
785+
'Memory: 50 fire-and-forget completed',
786+
)
787+
}
788+
} catch (e) {
789+
console.error('Memory test error:', e)
790+
}
791+
}}
792+
/>
793+
</TestSection>
794+
520795
<TestSection
521796
title="Response Tests"
522797
description="Test different response types and status codes."
@@ -642,6 +917,19 @@ export default function HttpTest() {
642917
Request/response bodies are recorded as
643918
configured
644919
</li>
920+
<li>
921+
Response Body Capture: each span has an{' '}
922+
<code>http.response.body</code> attribute with
923+
the full JSON response (not just{' '}
924+
<code>http.response.body.size</code>)
925+
</li>
926+
<li>
927+
Memory Pressure: after running stress tests,
928+
check console for heap delta logs and use
929+
DevTools Memory tab to verify the{' '}
930+
<code>pendingResponseAttributes</code> map
931+
drains and heap does not grow unbounded
932+
</li>
645933
</ul>
646934
</li>
647935
</ol>

0 commit comments

Comments
 (0)