Skip to content

Commit

Permalink
Merge branch 'main' into feat/capture-touch-breadcrumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
vaind authored Sep 4, 2024
2 parents ed4cab2 + a40bb7c commit 7563246
Show file tree
Hide file tree
Showing 23 changed files with 548 additions and 57 deletions.
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Features

- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208)).
- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208), [#2269](https://github.com/getsentry/sentry-dart/pull/2269)).

To try out replay, you can set following options (access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)):

Expand All @@ -19,6 +19,19 @@
);
```

- Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227))

```dart
await SentryFlutter.init(
(options) {
...
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
},
appRunner: () => runApp(MyApp()),
);
```

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

### Dependencies
Expand All @@ -27,12 +40,17 @@
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8360)
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.35.1...8.36.0)

### Fixes

- Only access renderObject if `hasSize` is true ([#2263](https://github.com/getsentry/sentry-dart/pull/2263))

## 8.8.0

### Features

- Add `SentryFlutter.nativeCrash()` using MethodChannels for Android and iOS ([#2239](https://github.com/getsentry/sentry-dart/pull/2239))
- This can be used to test if native crash reporting works

- Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218))
- This will ignore the Routes and prevent the Route from being pushed to the Sentry server.
- Ignored routes will also create no TTID and TTFD spans.
Expand Down
12 changes: 3 additions & 9 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'transport/rate_limiter.dart';
import 'transport/spotlight_http_transport.dart';
import 'transport/task_queue.dart';
import 'utils/isolate_utils.dart';
import 'utils/regex_utils.dart';
import 'utils/stacktrace_utils.dart';
import 'version.dart';

Expand Down Expand Up @@ -196,7 +197,7 @@ class SentryClient {
}

var message = event.message!.formatted;
return _isMatchingRegexPattern(message, _options.ignoreErrors);
return isMatchingRegexPattern(message, _options.ignoreErrors);
}

SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) {
Expand Down Expand Up @@ -415,7 +416,7 @@ class SentryClient {
}

var name = transaction.tracer.name;
return _isMatchingRegexPattern(name, _options.ignoreTransactions);
return isMatchingRegexPattern(name, _options.ignoreTransactions);
}

/// Reports the [envelope] to Sentry.io.
Expand Down Expand Up @@ -593,11 +594,4 @@ class SentryClient {
SentryId.empty(),
);
}

bool _isMatchingRegexPattern(String value, List<String> regexPattern,
{bool caseSensitive = false}) {
final combinedRegexPattern = regexPattern.join('|');
final regExp = RegExp(combinedRegexPattern, caseSensitive: caseSensitive);
return regExp.hasMatch(value);
}
}
2 changes: 2 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,12 @@ class SentryOptions {

/// The ignoreErrors tells the SDK which errors should be not sent to the sentry server.
/// If an null or an empty list is used, the SDK will send all transactions.
/// To use regex add the `^` and the `$` to the string.
List<String> ignoreErrors = [];

/// The ignoreTransactions tells the SDK which transactions should be not sent to the sentry server.
/// If null or an empty list is used, the SDK will send all transactions.
/// To use regex add the `^` and the `$` to the string.
List<String> ignoreTransactions = [];

final List<String> _inAppExcludes = [];
Expand Down
4 changes: 0 additions & 4 deletions dart/lib/src/transport/spotlight_http_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ class SpotlightHttpTransport extends Transport {
Future<void> _sendToSpotlight(SentryEnvelope envelope) async {
envelope.header.sentAt = _options.clock();

// Screenshots do not work currently https://github.com/getsentry/spotlight/issues/274
envelope.items
.removeWhere((element) => element.header.contentType == 'image/png');

final spotlightRequest = await _requestHandler.createRequest(envelope);

final response = await _options.httpClient
Expand Down
9 changes: 9 additions & 0 deletions dart/lib/src/utils/regex_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:meta/meta.dart';

@internal
bool isMatchingRegexPattern(String value, List<String> regexPattern,
{bool caseSensitive = false}) {
final combinedRegexPattern = regexPattern.join('|');
final regExp = RegExp(combinedRegexPattern, caseSensitive: caseSensitive);
return regExp.hasMatch(value);
}
24 changes: 24 additions & 0 deletions dart/test/utils/regex_utils_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:sentry/src/utils/regex_utils.dart';
import 'package:test/test.dart';

void main() {
group('regex_utils', () {
final testString = "this is a test";

test('testString contains string pattern', () {
expect(isMatchingRegexPattern(testString, ["is"]), isTrue);
});

test('testString does not contain string pattern', () {
expect(isMatchingRegexPattern(testString, ["not"]), isFalse);
});

test('testString contains regex pattern', () {
expect(isMatchingRegexPattern(testString, ["^this.*\$"]), isTrue);
});

test('testString does not contain regex pattern', () {
expect(isMatchingRegexPattern(testString, ["^is.*\$"]), isFalse);
});
});
}
10 changes: 10 additions & 0 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,8 @@ Future<void> showDialogWithTextAndImage(BuildContext context) async {
await DefaultAssetBundle.of(context).loadString('assets/lorem-ipsum.txt');

if (!context.mounted) return;
final imageBytes =
await DefaultAssetBundle.of(context).load('assets/sentry-wordmark.png');
await showDialog<void>(
context: context,

Check notice on line 1049 in flutter/example/lib/main.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

Don't use 'BuildContext's across async gaps.

Try rewriting the code to not use the 'BuildContext', or guard the use with a 'mounted' check. See https://dart.dev/diagnostics/use_build_context_synchronously to learn more about this problem.
// gets tracked if using SentryNavigatorObserver
Expand All @@ -1056,7 +1058,15 @@ Future<void> showDialogWithTextAndImage(BuildContext context) async {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Use various ways an image is included in the app.
// Local asset images are not obscured in replay recording.
Image.asset('assets/sentry-wordmark.png'),
Image.asset('assets/sentry-wordmark.png', bundle: rootBundle),
Image.asset('assets/sentry-wordmark.png',
bundle: DefaultAssetBundle.of(context)),
Image.network(
'https://www.gstatic.com/recaptcha/api2/logo_48.png'),
Image.memory(imageBytes.buffer.asUint8List()),
Text(text),
],
),
Expand Down
2 changes: 1 addition & 1 deletion flutter/ios/Classes/SentryFlutterPluginApple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
case "nativeCrash":
crash()

case "sendReplayForEvent":
case "captureReplay":
#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS))
PrivateSentrySDKOnly.captureReplay()
result(PrivateSentrySDKOnly.getReplayId())
Expand Down
2 changes: 1 addition & 1 deletion flutter/lib/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export 'src/sentry_flutter.dart';
export 'src/sentry_flutter_options.dart';
export 'src/sentry_replay_options.dart';
export 'src/flutter_sentry_attachment.dart';
export 'src/sentry_asset_bundle.dart';
export 'src/sentry_asset_bundle.dart' show SentryAssetBundle;
export 'src/integrations/on_error_integration.dart';
export 'src/screenshot/sentry_screenshot_widget.dart';
export 'src/screenshot/sentry_screenshot_quality.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'dart:html' as html show window, Window;

Check warning on line 1 in flutter/lib/src/event_processor/url_filter/html_url_filter_event_processor.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

Unused import: 'dart:html'.

Try removing the import directive. See https://dart.dev/diagnostics/unused_import to learn more about this problem.

import '../../../sentry_flutter.dart';
import 'url_filter_event_processor.dart';
// ignore: implementation_imports
import 'package:sentry/src/utils/regex_utils.dart';

// ignore_for_file: invalid_use_of_internal_member

UrlFilterEventProcessor urlFilterEventProcessor(SentryFlutterOptions options) =>
WebUrlFilterEventProcessor(options);

class WebUrlFilterEventProcessor implements UrlFilterEventProcessor {
WebUrlFilterEventProcessor(
this._options,
);

final SentryFlutterOptions _options;

@override
SentryEvent? apply(SentryEvent event, Hint hint) {
final frames = _getStacktraceFrames(event);
final lastPath = frames?.first?.absPath;

if (lastPath == null) {
return event;
}

if (_options.allowUrls.isNotEmpty &&
!isMatchingRegexPattern(lastPath, _options.allowUrls)) {
return null;
}

if (_options.denyUrls.isNotEmpty &&
isMatchingRegexPattern(lastPath, _options.denyUrls)) {
return null;
}

return event;
}

Iterable<SentryStackFrame?>? _getStacktraceFrames(SentryEvent event) {
if (event.exceptions?.isNotEmpty == true) {
return event.exceptions?.first.stackTrace?.frames;
}
if (event.threads?.isNotEmpty == true) {
final stacktraces = event.threads?.map((e) => e.stacktrace);
return stacktraces
?.where((element) => element != null)
.expand((element) => element!.frames);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import '../../../sentry_flutter.dart';
import 'url_filter_event_processor.dart';

UrlFilterEventProcessor urlFilterEventProcessor(SentryFlutterOptions _) =>
IoUrlFilterEventProcessor();

class IoUrlFilterEventProcessor implements UrlFilterEventProcessor {
@override
SentryEvent apply(SentryEvent event, Hint hint) => event;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import '../../../sentry_flutter.dart';
import 'io_url_filter_event_processor.dart'
if (dart.library.html) 'html_url_filter_event_processor.dart'
if (dart.library.js_interop) 'web_url_filter_event_processor.dart';

abstract class UrlFilterEventProcessor implements EventProcessor {
factory UrlFilterEventProcessor(SentryFlutterOptions options) =>
urlFilterEventProcessor(options);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// We would lose compatibility with old dart versions by adding web to pubspec.
// ignore: depend_on_referenced_packages
import 'package:web/web.dart' as web show window, Window;

Check warning on line 3 in flutter/lib/src/event_processor/url_filter/web_url_filter_event_processor.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

Unused import: 'package:web/web.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unused_import to learn more about this problem.

import '../../../sentry_flutter.dart';
import 'url_filter_event_processor.dart';
// ignore: implementation_imports
import 'package:sentry/src/utils/regex_utils.dart';

// ignore_for_file: invalid_use_of_internal_member

UrlFilterEventProcessor urlFilterEventProcessor(SentryFlutterOptions options) =>
WebUrlFilterEventProcessor(options);

class WebUrlFilterEventProcessor implements UrlFilterEventProcessor {
WebUrlFilterEventProcessor(
this._options,
);

final SentryFlutterOptions _options;

@override
SentryEvent? apply(SentryEvent event, Hint hint) {
final frames = _getStacktraceFrames(event);
final lastPath = frames?.first?.absPath;

if (lastPath == null) {
return event;
}

if (_options.allowUrls.isNotEmpty &&
!isMatchingRegexPattern(lastPath, _options.allowUrls)) {
return null;
}

if (_options.denyUrls.isNotEmpty &&
isMatchingRegexPattern(lastPath, _options.denyUrls)) {
return null;
}

return event;
}

Iterable<SentryStackFrame?>? _getStacktraceFrames(SentryEvent event) {
if (event.exceptions?.isNotEmpty == true) {
return event.exceptions?.first.stackTrace?.frames;
}
if (event.threads?.isNotEmpty == true) {
final stacktraces = event.threads?.map((e) => e.stacktrace);
return stacktraces
?.where((element) => element != null)
.expand((element) => element!.frames);
}
return null;
}
}
32 changes: 30 additions & 2 deletions flutter/lib/src/replay/widget_filter.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:sentry/sentry.dart';

import '../../sentry_flutter.dart';
import '../sentry_asset_bundle.dart';

@internal
class WidgetFilter {
Expand All @@ -14,11 +15,14 @@ class WidgetFilter {
late double _pixelRatio;
late Rect _bounds;
final _warnedWidgets = <int>{};
final AssetBundle _rootAssetBundle;

WidgetFilter(
{required this.redactText,
required this.redactImages,
required this.logger});
required this.logger,
@visibleForTesting AssetBundle? rootAssetBundle})
: _rootAssetBundle = rootAssetBundle ?? rootBundle;

void obscure(BuildContext context, double pixelRatio, Rect bounds) {
_pixelRatio = pixelRatio;
Expand Down Expand Up @@ -57,6 +61,14 @@ class WidgetFilter {
} else if (redactText && widget is EditableText) {
color = widget.style.color;
} else if (redactImages && widget is Image) {
if (widget.image is AssetBundleImageProvider) {
final image = widget.image as AssetBundleImageProvider;
if (isBuiltInAssetImage(image)) {
logger(SentryLevel.debug,
"WidgetFilter skipping asset: $widget ($image).");
return false;
}
}
color = widget.color;
} else {
// No other type is currently obscured.
Expand Down Expand Up @@ -115,6 +127,22 @@ class WidgetFilter {
return true;
}

@visibleForTesting
@pragma('vm:prefer-inline')
bool isBuiltInAssetImage(AssetBundleImageProvider image) {
late final AssetBundle? bundle;
if (image is AssetImage) {
bundle = image.bundle;
} else if (image is ExactAssetImage) {
bundle = image.bundle;
} else {
return false;
}
return (bundle == null ||
bundle == _rootAssetBundle ||
(bundle is SentryAssetBundle && bundle.bundle == _rootAssetBundle));
}

@pragma('vm:prefer-inline')
void _cantObscure(Widget widget, String message) {
if (!_warnedWidgets.contains(widget.hashCode)) {
Expand Down
Loading

0 comments on commit 7563246

Please sign in to comment.