Skip to content

Commit 6ff9f3a

Browse files
committed
feat(flutter): implement layout snapshots
1 parent 362bb35 commit 6ff9f3a

File tree

94 files changed

+5041
-1021
lines changed

Some content is hidden

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

94 files changed

+5041
-1021
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: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 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+
MaterialApp(
25+
home: Scaffold(
26+
appBar: AppBar(title: const Text('Perf Test')),
27+
body: _buildDeeplyNestedButton(50),
28+
),
29+
),
30+
);
31+
32+
await tester.pumpAndSettle();
33+
34+
final buttonFinder = find.byKey(const ValueKey('test_button'));
35+
expect(buttonFinder, findsOneWidget);
36+
37+
await binding.traceAction(
38+
() async {
39+
for (int i = 0; i < 30; i++) {
40+
await tester.tap(buttonFinder);
41+
await tester.pump();
42+
await Future.delayed(const Duration(milliseconds: 500));
43+
}
44+
},
45+
reportKey: 'layout_snapshot',
46+
);
47+
});
48+
}
49+
50+
Widget _buildDeeplyNestedButton(int depth) {
51+
return Stack(
52+
children: [
53+
_buildDeeplyNestedContainer(depth),
54+
Center(
55+
child: ElevatedButton(
56+
key: const ValueKey('test_button'),
57+
onPressed: () {},
58+
child: const Text('Tap Me'),
59+
),
60+
),
61+
],
62+
);
63+
}
64+
65+
Widget _buildDeeplyNestedContainer(int depth) {
66+
if (depth <= 0) {
67+
return const SizedBox.shrink();
68+
}
69+
70+
// Alternate between different container types for variety
71+
if (depth % 4 == 0) {
72+
return Container(
73+
padding: const EdgeInsets.all(1),
74+
child: _buildDeeplyNestedContainer(depth - 1),
75+
);
76+
} else if (depth % 4 == 1) {
77+
return Row(
78+
mainAxisSize: MainAxisSize.min,
79+
children: [
80+
Flexible(
81+
child: _buildDeeplyNestedContainer(depth - 1),
82+
),
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+
Flexible(
95+
child: _buildDeeplyNestedContainer(depth - 1),
96+
),
97+
],
98+
);
99+
}
100+
}

flutter/example/lib/main.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
44
import 'package:measure_flutter/measure_flutter.dart';
55
import 'package:measure_flutter_example/src/screen_main.dart';
66

7+
import 'msr_widgets.g.dart';
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)