Skip to content

Commit 3751dbc

Browse files
vaindbuenaflor
andauthored
feat: capture touch breadcrumbs for all buttons (#2242)
* chore: cleanup user interaction widget code * renames & more cleanup * more cleanup * more refactoring & clenaup before actual functional changes * more refactoring * feat: collect touch element path * update tests * add tests for the new support of non-keyed button presses * cleanup & improve existing code * chore: update changelog * update native replay integration with touch breadcrumb path * fix tests * Update CHANGELOG.md * linter issues --------- Co-authored-by: Giancarlo Buenaflor <[email protected]>
1 parent d5696bf commit 3751dbc

11 files changed

+506
-232
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
);
3939
```
4040

41+
- Collect touch breadcrumbs for all buttons, not just those with `key` specified. ([#2242](https://github.com/getsentry/sentry-dart/pull/2242))
42+
4143
### Dependencies
4244

4345
- Bump Cocoa SDK from v8.35.1 to v8.36.0 ([#2252](https://github.com/getsentry/sentry-dart/pull/2252))

dart/lib/src/protocol/breadcrumb.dart

+5-30
Original file line numberDiff line numberDiff line change
@@ -105,42 +105,17 @@ class Breadcrumb {
105105
String? viewId,
106106
String? viewClass,
107107
}) {
108-
final newData = data ?? {};
109-
var path = '';
110-
111-
if (viewId != null) {
112-
newData['view.id'] = viewId;
113-
path = viewId;
114-
}
115-
116-
if (newData.containsKey('label')) {
117-
if (path.isEmpty) {
118-
path = newData['label'];
119-
} else {
120-
path = "$path, label: ${newData['label']}";
121-
}
122-
}
123-
124-
if (viewClass != null) {
125-
newData['view.class'] = viewClass;
126-
if (path.isEmpty) {
127-
path = viewClass;
128-
} else {
129-
path = "$viewClass($path)";
130-
}
131-
}
132-
133-
if (path.isNotEmpty && !newData.containsKey('path')) {
134-
newData['path'] = path;
135-
}
136-
137108
return Breadcrumb(
138109
message: message,
139110
level: level,
140111
category: 'ui.$subCategory',
141112
type: 'user',
142113
timestamp: timestamp,
143-
data: newData,
114+
data: {
115+
if (viewId != null) 'view.id': viewId,
116+
if (viewClass != null) 'view.class': viewClass,
117+
if (data != null) ...data,
118+
},
144119
);
145120
}
146121

dart/test/protocol/breadcrumb_test.dart

-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,6 @@ void main() {
222222
'foo': 'bar',
223223
'view.id': 'foo',
224224
'view.class': 'bar',
225-
'path': 'bar(foo)',
226225
},
227226
});
228227
});

flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayBreadcrumbConverter.kt

+33-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import io.sentry.rrweb.RRWebSpanEvent
88
import java.util.Date
99

1010
private const val MILLIS_PER_SECOND = 1000.0
11+
private const val MAX_PATH_ITEMS = 4
12+
private const val MAX_PATH_IDENTIFIER_LENGTH = 20
1113

1214
class SentryFlutterReplayBreadcrumbConverter : DefaultReplayBreadcrumbConverter() {
1315
internal companion object {
@@ -30,7 +32,7 @@ class SentryFlutterReplayBreadcrumbConverter : DefaultReplayBreadcrumbConverter(
3032
"ui.click" ->
3133
newRRWebBreadcrumb(breadcrumb).apply {
3234
category = "ui.tap"
33-
message = breadcrumb.data["path"] as String?
35+
message = getTouchPathMessage(breadcrumb.data["path"])
3436
}
3537

3638
else -> {
@@ -83,4 +85,34 @@ class SentryFlutterReplayBreadcrumbConverter : DefaultReplayBreadcrumbConverter(
8385
}
8486
return rrWebEvent
8587
}
88+
89+
private fun getTouchPathMessage(maybePath: Any?): String? {
90+
if (maybePath !is List<*> || maybePath.isEmpty()) {
91+
return null
92+
}
93+
94+
val message = StringBuilder()
95+
for (i in Math.min(MAX_PATH_ITEMS, maybePath.size) - 1 downTo 0) {
96+
val item = maybePath[i]
97+
if (item !is Map<*, *>) {
98+
continue
99+
}
100+
101+
message.append(item["element"] ?: "?")
102+
103+
var identifier = item["label"] ?: item["name"]
104+
if (identifier is String && identifier.isNotEmpty()) {
105+
if (identifier.length > MAX_PATH_IDENTIFIER_LENGTH) {
106+
identifier = identifier.substring(0, MAX_PATH_IDENTIFIER_LENGTH - "...".length) + "..."
107+
}
108+
message.append("(").append(identifier).append(")")
109+
}
110+
111+
if (i > 0) {
112+
message.append(" > ")
113+
}
114+
}
115+
116+
return message.toString()
117+
}
86118
}

flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal class SentryFlutterReplayRecorder(
1212
private val channel: MethodChannel,
1313
private val integration: ReplayIntegration,
1414
) : Recorder {
15-
override fun start(config: ScreenshotRecorderConfig) {
15+
override fun start(recorderConfig: ScreenshotRecorderConfig) {
1616
val cacheDirPath = integration.replayCacheDir?.absolutePath
1717
if (cacheDirPath == null) {
1818
Log.w("Sentry", "Replay cache directory is null, can't start replay recorder.")
@@ -24,9 +24,9 @@ internal class SentryFlutterReplayRecorder(
2424
"ReplayRecorder.start",
2525
mapOf(
2626
"directory" to cacheDirPath,
27-
"width" to config.recordingWidth,
28-
"height" to config.recordingHeight,
29-
"frameRate" to config.frameRate,
27+
"width" to recorderConfig.recordingWidth,
28+
"height" to recorderConfig.recordingHeight,
29+
"frameRate" to recorderConfig.frameRate,
3030
"replayId" to integration.getReplayId().toString(),
3131
),
3232
)

flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.m

+37-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ - (instancetype _Nonnull)init {
3838
if ([breadcrumb.category isEqualToString:@"ui.click"]) {
3939
return [self convertFrom:breadcrumb
4040
withCategory:@"ui.tap"
41-
andMessage:breadcrumb.data[@"path"]];
41+
andMessage:[self getTouchPathMessage:breadcrumb.data[@"path"]]];
4242
}
4343

4444
SentryRRWebEvent *nativeBreadcrumb =
@@ -112,6 +112,42 @@ - (NSDate *_Nonnull)dateFrom:(NSNumber *_Nonnull)timestamp {
112112
return [NSDate dateWithTimeIntervalSince1970:(timestamp.doubleValue / 1000)];
113113
}
114114

115+
- (NSString * _Nullable)getTouchPathMessage:(id _Nullable)maybePath {
116+
if (![maybePath isKindOfClass:[NSArray class]]) {
117+
return nil;
118+
}
119+
120+
NSArray *path = (NSArray *)maybePath;
121+
if (path.count == 0) {
122+
return nil;
123+
}
124+
125+
NSMutableString *message = [NSMutableString string];
126+
for (NSInteger i = MIN(3, path.count - 1); i >= 0; i--) {
127+
id item = path[i];
128+
if (![item isKindOfClass:[NSDictionary class]]) {
129+
continue;
130+
}
131+
132+
NSDictionary *itemDict = (NSDictionary *)item;
133+
[message appendString:itemDict[@"element"] ?: @"?"];
134+
135+
id identifier = itemDict[@"label"] ?: itemDict[@"name"];
136+
if ([identifier isKindOfClass:[NSString class]] && [(NSString *)identifier length] > 0) {
137+
NSString *identifierStr = (NSString *)identifier;
138+
if (identifierStr.length > 20) {
139+
identifierStr = [[identifierStr substringToIndex:17] stringByAppendingString:@"..."];
140+
}
141+
[message appendFormat:@"(%@)", identifierStr];
142+
}
143+
144+
if (i > 0) {
145+
[message appendString:@" > "];
146+
}
147+
}
148+
149+
return message.length > 0 ? message : nil;
150+
}
115151
@end
116152

117153
#endif

flutter/lib/src/sentry_flutter_options.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,14 @@ class SentryFlutterOptions extends SentryOptions {
202202
///
203203
/// Requires adding the [SentryUserInteractionWidget] to the widget tree.
204204
/// Example:
205-
/// runApp(SentryUserInteractionWidget(child: App()));
205+
/// runApp(SentryWidget(child: App()));
206206
bool enableUserInteractionBreadcrumbs = true;
207207

208208
/// Enables the Auto instrumentation for user interaction tracing.
209209
///
210210
/// Requires adding the [SentryUserInteractionWidget] to the widget tree.
211211
/// Example:
212-
/// runApp(SentryUserInteractionWidget(child: App()));
212+
/// runApp(SentryWidget(child: App()));
213213
bool enableUserInteractionTracing = true;
214214

215215
/// Enable or disable the tracing of time to full display (TTFD).

0 commit comments

Comments
 (0)