Skip to content

Commit 9e26b8c

Browse files
committed
feat(flutter): implement layout snapshots
1 parent 6ce1546 commit 9e26b8c

File tree

93 files changed

+4841
-993
lines changed

Some content is hidden

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

93 files changed

+4841
-993
lines changed

android/measure/src/main/java/sh/measure/android/events/AttachmentType.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sh.measure.android.events
33
internal object AttachmentType {
44
const val SCREENSHOT = "screenshot"
55
const val LAYOUT_SNAPSHOT = "layout_snapshot"
6+
const val LAYOUT_SNAPSHOT_JSON = "layout_snapshot_json"
67

7-
val VALID_TYPES = listOf(SCREENSHOT, LAYOUT_SNAPSHOT)
8+
val VALID_TYPES = listOf(SCREENSHOT, LAYOUT_SNAPSHOT, LAYOUT_SNAPSHOT_JSON)
89
}

android/measure/src/main/java/sh/measure/android/events/InternalSignalCollector.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ internal class InternalSignalCollector(
117117
type = eventType,
118118
attributes = attributes,
119119
userDefinedAttributes = userDefinedAttrs,
120+
attachments = eventAttachments,
120121
userTriggered = userTriggered,
121122
)
122123
}
@@ -129,6 +130,7 @@ internal class InternalSignalCollector(
129130
type = eventType,
130131
attributes = attributes,
131132
userDefinedAttributes = userDefinedAttrs,
133+
attachments = eventAttachments,
132134
userTriggered = userTriggered,
133135
)
134136
}

android/measure/src/main/java/sh/measure/android/exporter/AttachmentExporter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ internal class DefaultAttachmentExporter(
134134
val response = httpClient.uploadFile(
135135
url = attachment.url,
136136
contentType = attachment.contentType,
137+
contentEncoding = attachment.contentEncoding,
137138
headers = attachment.headers,
138139
fileSize = file.length(),
139140
) { sink ->

android/measure/src/main/java/sh/measure/android/exporter/AttachmentPacket.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal data class AttachmentPacket(
1717
) {
1818
val contentType: String = when (type) {
1919
AttachmentType.LAYOUT_SNAPSHOT -> "image/svg+xml"
20+
AttachmentType.LAYOUT_SNAPSHOT_JSON -> "application/json"
2021
AttachmentType.SCREENSHOT -> {
2122
if (name.endsWith(".webp")) {
2223
"image/webp"
@@ -27,4 +28,9 @@ internal data class AttachmentPacket(
2728

2829
else -> "application/octet-stream"
2930
}
31+
32+
val contentEncoding: String? = when (type) {
33+
AttachmentType.LAYOUT_SNAPSHOT_JSON -> "gzip"
34+
else -> null
35+
}
3036
}

android/measure/src/main/java/sh/measure/android/exporter/HttpUrlConnectionClient.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal interface HttpClient {
2222
fun uploadFile(
2323
url: String,
2424
contentType: String,
25+
contentEncoding: String?,
2526
headers: Map<String, String>,
2627
fileSize: Long,
2728
fileWriter: (BufferedSink) -> Unit,
@@ -89,13 +90,15 @@ internal class HttpUrlConnectionClient(private val logger: Logger) : HttpClient
8990
override fun uploadFile(
9091
url: String,
9192
contentType: String,
93+
contentEncoding: String?,
9294
headers: Map<String, String>,
9395
fileSize: Long,
9496
fileWriter: (BufferedSink) -> Unit,
9597
): HttpResponse {
9698
var connection: HttpURLConnection? = null
9799
return try {
98-
connection = createFileUploadConnection(url, contentType, headers, fileSize)
100+
connection =
101+
createFileUploadConnection(url, contentType, contentEncoding, headers, fileSize)
99102
logger.log(LogLevel.Debug, "Uploading file to: $url")
100103
connection.outputStream.sink().buffer().use { sink ->
101104
fileWriter(sink)
@@ -144,6 +147,7 @@ internal class HttpUrlConnectionClient(private val logger: Logger) : HttpClient
144147
private fun createFileUploadConnection(
145148
url: String,
146149
contentType: String,
150+
contentEncoding: String?,
147151
headers: Map<String, String>,
148152
fileSize: Long,
149153
): HttpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply {
@@ -156,6 +160,7 @@ internal class HttpUrlConnectionClient(private val logger: Logger) : HttpClient
156160
setChunkedStreamingMode(0)
157161
}
158162
setRequestProperty("Content-Type", contentType)
163+
contentEncoding?.let { setRequestProperty("Content-Encoding", contentEncoding) }
159164
headers.forEach { (key, value) ->
160165
setRequestProperty(key, value)
161166
}

flutter/example/android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525

2626
<meta-data
2727
android:name="sh.measure.android.API_KEY"
28-
android:value="msrsh_" />
28+
android:value="msrsh_8456989a9cd452c7a4864d37a1f3cf9b4f8a45395203c19cb5b6d8252c6970fd_95a8744f" />
2929

3030
<meta-data android:name="sh.measure.android.API_URL"
31-
android:value="http://localhost:8080" />
31+
android:value="https://staging-ingest.measure.sh" />
3232

3333
<meta-data
3434
android:name="flutterEmbedding"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package sh.measure.android.flutter
22

33
import android.app.Application
4+
import sh.measure.android.Measure
5+
import sh.measure.android.config.MeasureConfig
46

57
class SampleApp : Application() {
68
override fun onCreate() {
79
super.onCreate()
10+
11+
Measure.init(this, MeasureConfig(enableLogging = true))
812
}
913
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:integration_test/integration_test.dart';
4+
import 'package:measure_flutter/measure_flutter.dart';
5+
6+
void main() {
7+
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
8+
9+
testWidgets('Layout snapshot captureTree performance test', (WidgetTester tester) async {
10+
await Measure.instance.init(
11+
() {},
12+
config: const MeasureConfig(
13+
enableLogging: false,
14+
autoStart: true,
15+
trackScreenshotOnCrash: false,
16+
),
17+
clientInfo: ClientInfo(
18+
apiKey: "test_key",
19+
apiUrl: "https://test.measure.sh",
20+
),
21+
);
22+
23+
await tester.pumpWidget(
24+
MeasureWidget(
25+
child: MaterialApp(
26+
home: Scaffold(
27+
appBar: AppBar(title: const Text('Perf Test')),
28+
body: _buildDeeplyNestedButton(500),
29+
),
30+
),
31+
),
32+
);
33+
34+
await tester.pumpAndSettle();
35+
36+
final buttonFinder = find.byKey(const ValueKey('test_button'));
37+
expect(buttonFinder, findsOneWidget);
38+
39+
await binding.traceAction(
40+
() async {
41+
for (int i = 0; i < 30; i++) {
42+
await tester.tap(buttonFinder);
43+
await tester.pump();
44+
await Future.delayed(const Duration(milliseconds: 500));
45+
}
46+
},
47+
reportKey: 'layout_snapshot',
48+
);
49+
});
50+
}
51+
52+
Widget _buildDeeplyNestedButton(int depth) {
53+
return Stack(
54+
children: [
55+
_buildDeeplyNestedContainer(depth),
56+
Center(
57+
child: ElevatedButton(
58+
key: const ValueKey('test_button'),
59+
onPressed: () {},
60+
child: const Text('Tap Me'),
61+
),
62+
),
63+
],
64+
);
65+
}
66+
67+
Widget _buildDeeplyNestedContainer(int depth) {
68+
if (depth <= 0) {
69+
return const SizedBox.shrink();
70+
}
71+
72+
// Alternate between different container types for variety
73+
if (depth % 4 == 0) {
74+
return Container(
75+
padding: const EdgeInsets.all(1),
76+
child: _buildDeeplyNestedContainer(depth - 1),
77+
);
78+
} else if (depth % 4 == 1) {
79+
return Row(
80+
mainAxisSize: MainAxisSize.min,
81+
children: [
82+
_buildDeeplyNestedContainer(depth - 1),
83+
],
84+
);
85+
} else if (depth % 4 == 2) {
86+
return Container(
87+
margin: const EdgeInsets.all(1),
88+
child: _buildDeeplyNestedContainer(depth - 1),
89+
);
90+
} else {
91+
return Column(
92+
mainAxisSize: MainAxisSize.min,
93+
children: [
94+
_buildDeeplyNestedContainer(depth - 1),
95+
],
96+
);
97+
}
98+
}

flutter/example/lib/main.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import 'dart:async';
22

33
import 'package:flutter/material.dart';
44
import 'package:measure_flutter/measure_flutter.dart';
5+
import 'package:measure_flutter_example/src/msr/msr_widgets.g.dart';
56
import 'package:measure_flutter_example/src/screen_main.dart';
67

8+
79
Future<void> main() async {
810
await Measure.instance.init(
9-
() => runApp(MeasureWidget(child: MyApp())),
11+
() => runApp(MeasureWidget(child: MyApp())),
1012
config: const MeasureConfig(
1113
enableLogging: true,
1214
trackScreenshotOnCrash: true,
@@ -16,10 +18,11 @@ Future<void> main() async {
1618
autoStart: true,
1719
traceSamplingRate: 1,
1820
samplingRateForErrorFreeSessions: 1,
21+
widgetFilter: widgetFilter,
1922
),
2023
clientInfo: ClientInfo(
21-
apiKey: "msrsh-123",
22-
apiUrl: "http://localhost:8080",
24+
apiKey: "msrsh_8456989a9cd452c7a4864d37a1f3cf9b4f8a45395203c19cb5b6d8252c6970fd_95a8744f",
25+
apiUrl: "https://staging-ingest.measure.sh",
2326
),
2427
);
2528
}
@@ -51,4 +54,4 @@ class MyApp extends StatelessWidget {
5154
home: MainScreen(),
5255
);
5356
}
54-
}
57+
}

flutter/example/lib/msr_widgets.g.dart

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)