diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 98d4f67..ff37de0 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -55,10 +55,14 @@ steps: - "--app=/app/features/fixtures/mazerunner/build/ios/ipa/mazerunner.ipa" - "--farm=bb" - "--device=IOS_14|IOS_15|IOS_16" + - "--fail-fast" - "--no-tunnel" - "--aws-public-ip" - - "--fail-fast" - "--appium-version=1.22" + test-collector#v1.10.2: + files: "reports/TEST-*.xml" + format: "junit" + branch: "^main|next$$" concurrency: 25 concurrency_group: 'bitbar' concurrency_method: eager @@ -102,10 +106,14 @@ steps: - "--app=features/fixtures/mazerunner/build/app/outputs/flutter-apk/app-release.apk" - "--farm=bb" - "--device=ANDROID_10|ANDROID_11|ANDROID_12|ANDROID_13" + - "--fail-fast" - "--no-tunnel" - "--aws-public-ip" - - "--fail-fast" - "--appium-version=1.22" + test-collector#v1.10.2: + files: "reports/TEST-*.xml" + format: "junit" + branch: "^main|next$$" concurrency: 25 concurrency_group: 'bitbar' concurrency_method: eager diff --git a/CHANGELOG.md b/CHANGELOG.md index c25f66f..7423fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ Changelog ========= +## 1.3.0 (2024-09-30) + +### Enhancements + +* Custom attributes can now be set on a span, including as lists of primitives (int, double, bool, String) [88](https://github.com/bugsnag/bugsnag-flutter-performance/pull/88) + +* Introduced OnSpanEndCallbacks that allow changes to spans when their end() method is called, but before they are sent [88](https://github.com/bugsnag/bugsnag-flutter-performance/pull/88) + ## 1.2.1 (2024-09-18) ### Bug fixes diff --git a/VERSION b/VERSION index 6085e94..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.1 +1.3.0 diff --git a/docker-compose.yml b/docker-compose.yml index ddceb54..5aeee6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,14 +20,17 @@ services: BUILDKITE_REPO: BUILDKITE_RETRY_COUNT: BUILDKITE_STEP_KEY: + BUILDKITE_ANALYTICS_TOKEN: MAZE_BUGSNAG_API_KEY: MAZE_REPEATER_API_KEY: + MAZE_NO_FAIL_FAST: ports: - "9000-9499:9339" volumes: - ./features/:/app/features/ - ./maze_output:/app/maze_output - /var/run/docker.sock:/var/run/docker.sock + - ./reports/:/app/reports/ networks: default: diff --git a/features/fixture_resources/lib/channels.dart b/features/fixture_resources/lib/channels.dart index d270452..70d2b4a 100644 --- a/features/fixture_resources/lib/channels.dart +++ b/features/fixture_resources/lib/channels.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:flutter/services.dart'; class MazeRunnerChannels { diff --git a/features/fixture_resources/lib/main.dart b/features/fixture_resources/lib/main.dart index 59c43de..fa04dcb 100644 --- a/features/fixture_resources/lib/main.dart +++ b/features/fixture_resources/lib/main.dart @@ -84,7 +84,7 @@ class Command { } class MazeRunnerFlutterApp extends StatelessWidget { - const MazeRunnerFlutterApp({Key? key}) : super(key: key); + const MazeRunnerFlutterApp({super.key}); @override Widget build(BuildContext context) { @@ -151,9 +151,9 @@ class MazeRunnerHomePage extends StatefulWidget { final String mazerunnerUrl; const MazeRunnerHomePage({ - Key? key, + super.key, required this.mazerunnerUrl, - }) : super(key: key); + }); @override State createState() => _HomePageState(); @@ -332,9 +332,9 @@ class _HomePageState extends State { height: 400.0, width: double.infinity, child: TextButton( - child: const Text("Run Command"), onPressed: () => _onRunCommand(context), key: const Key("runCommand"), + child: const Text("Run Command"), ), ), TextField( @@ -373,14 +373,14 @@ class _HomePageState extends State { ), ), TextButton( - child: const Text("Start Bugsnag"), onPressed: _onStartBugsnag, key: const Key("startBugsnag"), + child: const Text("Start Bugsnag"), ), TextButton( - child: const Text("Run Scenario"), onPressed: () => _onRunScenario(context), key: const Key("startScenario"), + child: const Text("Run Scenario"), ), ], ), diff --git a/features/fixture_resources/lib/scenarios/auto_instrument_app_starts_scenario.dart b/features/fixture_resources/lib/scenarios/auto_instrument_app_starts_scenario.dart index f19836a..b56519b 100644 --- a/features/fixture_resources/lib/scenarios/auto_instrument_app_starts_scenario.dart +++ b/features/fixture_resources/lib/scenarios/auto_instrument_app_starts_scenario.dart @@ -10,8 +10,8 @@ class AutoInstrumentAppStartsScenario extends Scenario { bugsnag_performance.setExtraConfig("probabilityValueExpireTime", 1000); bugsnag_performance.start( apiKey: '12312312312312312312312312312312', - endpoint: Uri.parse(FixtureConfig.MAZE_HOST.toString() + '/traces')); - bugsnag_performance.measureRunApp(() async => await Duration(seconds: 1)); + endpoint: Uri.parse('${FixtureConfig.MAZE_HOST}/traces')); + bugsnag_performance.measureRunApp(() async => const Duration(seconds: 1)); setMaxBatchSize(4); } } diff --git a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_defer_scenario.dart b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_defer_scenario.dart index 58c61b9..d50187d 100644 --- a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_defer_scenario.dart +++ b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_defer_scenario.dart @@ -1,6 +1,5 @@ import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:flutter/material.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; @@ -68,7 +67,7 @@ class _AutoInstrumentNavigationBasicDeferScenarioScreenState onTap: () => widget.runCommandCallback(), ); } - return Text('AutoInstrumentNavigationBasicDeferScenarioScreen'); + return const Text('AutoInstrumentNavigationBasicDeferScenarioScreen'); } void setStage(int stage) { diff --git a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_scenario.dart b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_scenario.dart index 4c9f2f6..978b6e0 100644 --- a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_scenario.dart +++ b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_basic_scenario.dart @@ -1,7 +1,5 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; @@ -15,11 +13,11 @@ class AutoInstrumentNavigationBasicScenario extends Scenario { @override Widget? createWidget() { - return Text('AutoInstrumentNavigationBasicScenario'); + return const Text('AutoInstrumentNavigationBasicScenario'); } @override RouteSettings? routeSettings() { - return RouteSettings(name: 'basic_navigation_scenario'); + return const RouteSettings(name: 'basic_navigation_scenario'); } } diff --git a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_complex_defer_scenario.dart b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_complex_defer_scenario.dart index 53536ca..64bb82d 100644 --- a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_complex_defer_scenario.dart +++ b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_complex_defer_scenario.dart @@ -1,6 +1,5 @@ import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:flutter/material.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; diff --git a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_nested_navigation_scenario.dart b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_nested_navigation_scenario.dart index 1803414..d6bd7db 100644 --- a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_nested_navigation_scenario.dart +++ b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_nested_navigation_scenario.dart @@ -1,7 +1,6 @@ import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:flutter/material.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; diff --git a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_push_and_pop_scenario.dart b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_push_and_pop_scenario.dart index d52f5d7..3b1a964 100644 --- a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_push_and_pop_scenario.dart +++ b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_push_and_pop_scenario.dart @@ -1,6 +1,4 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:flutter/material.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; @@ -62,8 +60,8 @@ class AutoInstrumentNavigationPushAndPopScenarioScreenState Widget build(BuildContext context) { return GestureDetector( child: Container( - child: Text('AutoInstrumentNavigationPushAndPopScenarioScreen'), color: Colors.white, + child: Text('AutoInstrumentNavigationPushAndPopScenarioScreen'), ), onTap: () => widget.runCommandCallback(), ); diff --git a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_with_view_load_scenario.dart b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_with_view_load_scenario.dart index cfe573b..cbc3fef 100644 --- a/features/fixture_resources/lib/scenarios/auto_instrument_navigation_with_view_load_scenario.dart +++ b/features/fixture_resources/lib/scenarios/auto_instrument_navigation_with_view_load_scenario.dart @@ -1,6 +1,5 @@ import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:flutter/material.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; diff --git a/features/fixture_resources/lib/scenarios/correlation_null_scenario.dart b/features/fixture_resources/lib/scenarios/correlation_null_scenario.dart index d30a150..f490897 100644 --- a/features/fixture_resources/lib/scenarios/correlation_null_scenario.dart +++ b/features/fixture_resources/lib/scenarios/correlation_null_scenario.dart @@ -1,6 +1,5 @@ import 'package:bugsnag_flutter/bugsnag_flutter.dart'; import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; diff --git a/features/fixture_resources/lib/scenarios/correlation_simple_scenario.dart b/features/fixture_resources/lib/scenarios/correlation_simple_scenario.dart index e2f2f03..45d1f43 100644 --- a/features/fixture_resources/lib/scenarios/correlation_simple_scenario.dart +++ b/features/fixture_resources/lib/scenarios/correlation_simple_scenario.dart @@ -1,6 +1,5 @@ import 'package:bugsnag_flutter/bugsnag_flutter.dart'; import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; -import 'package:mazerunner/main.dart'; import 'scenario.dart'; diff --git a/features/fixture_resources/lib/scenarios/custom_app_version_scenario.dart b/features/fixture_resources/lib/scenarios/custom_app_version_scenario.dart index e4c3874..0f3ce10 100644 --- a/features/fixture_resources/lib/scenarios/custom_app_version_scenario.dart +++ b/features/fixture_resources/lib/scenarios/custom_app_version_scenario.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'scenario.dart'; class CustomAppVersionScenario extends Scenario { diff --git a/features/fixture_resources/lib/scenarios/custom_enabled_release_stage_scenario.dart b/features/fixture_resources/lib/scenarios/custom_enabled_release_stage_scenario.dart index c9677c3..8e6cef2 100644 --- a/features/fixture_resources/lib/scenarios/custom_enabled_release_stage_scenario.dart +++ b/features/fixture_resources/lib/scenarios/custom_enabled_release_stage_scenario.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'scenario.dart'; class CustomEnabledReleaseStageScenario extends Scenario { diff --git a/features/fixture_resources/lib/scenarios/custom_release_stage_scenario.dart b/features/fixture_resources/lib/scenarios/custom_release_stage_scenario.dart index 6a521c9..93e0ac7 100644 --- a/features/fixture_resources/lib/scenarios/custom_release_stage_scenario.dart +++ b/features/fixture_resources/lib/scenarios/custom_release_stage_scenario.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'scenario.dart'; class CustomReleaseStageScenario extends Scenario { diff --git a/features/fixture_resources/lib/scenarios/custom_service_name_scenario.dart b/features/fixture_resources/lib/scenarios/custom_service_name_scenario.dart index d217547..54484b2 100644 --- a/features/fixture_resources/lib/scenarios/custom_service_name_scenario.dart +++ b/features/fixture_resources/lib/scenarios/custom_service_name_scenario.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'scenario.dart'; class CustomServiceNameScenario extends Scenario { diff --git a/features/fixture_resources/lib/scenarios/custom_span_attributes_scenario.dart b/features/fixture_resources/lib/scenarios/custom_span_attributes_scenario.dart new file mode 100644 index 0000000..36e542e --- /dev/null +++ b/features/fixture_resources/lib/scenarios/custom_span_attributes_scenario.dart @@ -0,0 +1,49 @@ +import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; + +import 'scenario.dart'; + +class CustomSpanAttributesScenario extends Scenario { + @override + Future run() async { + await startBugsnag(onSpanEndCallbacks: [ + _setAttributesAndThrow, + _discardUnwantedSpan, + _setAttributes, + ]); + setMaxBatchSize(1); + doSimpleSpan('CustomSpanAttributesScenarioDiscaredSpan'); + final span = + bugsnag_performance.startSpan('CustomSpanAttributesScenarioSpan'); + span.setAttribute('customAttribute1', 42); + span.setAttribute('customAttribute2', 'Test'); + span.setAttribute('customAttribute3', 1); + span.end(); + span.setAttribute('customAttribute6', 'T'); + } + + Future _setAttributesAndThrow(BugsnagPerformanceSpan span) async { + span.setAttribute('customAttribute1', 'C'); + throw Exception(''); + } + + Future _discardUnwantedSpan(BugsnagPerformanceSpan span) async { + if (span.name == 'CustomSpanAttributesScenarioDiscaredSpan') { + return false; + } + return true; + } + + Future _setAttributes(BugsnagPerformanceSpan span) async { + span.setAttribute('customAttribute3', 2); + span.setAttribute('customAttribute3', 3); + span.setAttribute('customAttribute2', null); + span.setAttribute('customAttribute4', 42.0); + span.setAttribute('customAttribute5', [ + 'customString', + 42, + true, + 43.0, + ]); + return true; + } +} diff --git a/features/fixture_resources/lib/scenarios/custom_span_attributes_with_limits_scenario.dart b/features/fixture_resources/lib/scenarios/custom_span_attributes_with_limits_scenario.dart new file mode 100644 index 0000000..bd103fb --- /dev/null +++ b/features/fixture_resources/lib/scenarios/custom_span_attributes_with_limits_scenario.dart @@ -0,0 +1,35 @@ +import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; + +import 'scenario.dart'; + +class CustomSpanAttributesWithLimitsScenario extends Scenario { + @override + Future run() async { + await startBugsnag( + attributeCountLimit: 8, + attributeStringValueLimit: 20, + attributeArrayLengthLimit: 6, + onSpanEndCallbacks: [ + _setAttributes, + ]); + setMaxBatchSize(1); + final span = bugsnag_performance + .startSpan('CustomSpanAttributesWithLimitsScenarioSpan'); + final tooLongKey = 'a' * 129; + span.setAttribute('customAttribute1', 42); + span.setAttribute(tooLongKey, 'Test'); + span.setAttribute('customAttribute2', 1); + span.setAttribute('customAttribute3', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + span.setAttribute( + 'customAttribute4', 'VeryLongStringAttributeValueThatExceedsTheLimit'); + span.setAttribute('customAttribute5', 42.0); + span.setAttribute('customAttribute6', 'Dropped'); + span.end(); + } + + Future _setAttributes(BugsnagPerformanceSpan span) async { + span.setAttribute('customAttribute7', 'Droppedtoo'); + span.setAttribute('customAttribute2', 'NotDropped'); + return true; + } +} diff --git a/features/fixture_resources/lib/scenarios/dio_callback_cancel_span.dart b/features/fixture_resources/lib/scenarios/dio_callback_cancel_span.dart index 6424be0..24a1e3a 100644 --- a/features/fixture_resources/lib/scenarios/dio_callback_cancel_span.dart +++ b/features/fixture_resources/lib/scenarios/dio_callback_cancel_span.dart @@ -1,5 +1,4 @@ import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; -import 'package:bugsnag_http_client/bugsnag_http_client.dart'; import 'package:dio/io.dart'; import '../main.dart'; import 'scenario.dart'; diff --git a/features/fixture_resources/lib/scenarios/dio_callback_edit_scenario.dart b/features/fixture_resources/lib/scenarios/dio_callback_edit_scenario.dart index 1e74cbf..53f8741 100644 --- a/features/fixture_resources/lib/scenarios/dio_callback_edit_scenario.dart +++ b/features/fixture_resources/lib/scenarios/dio_callback_edit_scenario.dart @@ -1,5 +1,4 @@ import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; -import 'package:bugsnag_http_client/bugsnag_http_client.dart'; import 'package:dio/io.dart'; import '../main.dart'; import 'scenario.dart'; diff --git a/features/fixture_resources/lib/scenarios/disable_custom_release_stage_scenario.dart b/features/fixture_resources/lib/scenarios/disable_custom_release_stage_scenario.dart index da25733..14a4be9 100644 --- a/features/fixture_resources/lib/scenarios/disable_custom_release_stage_scenario.dart +++ b/features/fixture_resources/lib/scenarios/disable_custom_release_stage_scenario.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'scenario.dart'; class DisableCustomReleaseStageScenario extends Scenario { diff --git a/features/fixture_resources/lib/scenarios/http_client_trace_propagation_urls_scenario.dart b/features/fixture_resources/lib/scenarios/http_client_trace_propagation_urls_scenario.dart index fc79e20..9422159 100644 --- a/features/fixture_resources/lib/scenarios/http_client_trace_propagation_urls_scenario.dart +++ b/features/fixture_resources/lib/scenarios/http_client_trace_propagation_urls_scenario.dart @@ -11,7 +11,7 @@ class HttpClientTracePropagationUrlsScenario extends Scenario { http.addSubscriber(bugsnag_performance.networkInstrumentation); http.get( Uri.parse('${FixtureConfig.MAZE_HOST.toString()}/reflect?dontsend')); - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); http.get(Uri.parse('${FixtureConfig.MAZE_HOST.toString()}/reflect?dosend')); } } diff --git a/features/fixture_resources/lib/scenarios/initial_p_scenario.dart b/features/fixture_resources/lib/scenarios/initial_p_scenario.dart index 6ae46e7..5b3f768 100644 --- a/features/fixture_resources/lib/scenarios/initial_p_scenario.dart +++ b/features/fixture_resources/lib/scenarios/initial_p_scenario.dart @@ -1,10 +1,7 @@ -import 'dart:convert'; - import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:mazerunner/main.dart'; import 'scenario.dart'; -import 'package:http/http.dart' as http; class InitialPScenario extends Scenario { @override @@ -15,7 +12,7 @@ class InitialPScenario extends Scenario { bugsnag_performance.setExtraConfig("probabilityValueExpireTime", 25000); bugsnag_performance.start( apiKey: '12312312312312312312312312312312', - endpoint: Uri.parse(FixtureConfig.MAZE_HOST.toString() + '/traces')); + endpoint: Uri.parse('${FixtureConfig.MAZE_HOST}/traces')); setMaxBatchSize(1); doSimpleSpan('First'); } diff --git a/features/fixture_resources/lib/scenarios/manual_span_scenario.dart b/features/fixture_resources/lib/scenarios/manual_span_scenario.dart index e1774b8..36a4252 100644 --- a/features/fixture_resources/lib/scenarios/manual_span_scenario.dart +++ b/features/fixture_resources/lib/scenarios/manual_span_scenario.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'scenario.dart'; class ManualSpanScenario extends Scenario { diff --git a/features/fixture_resources/lib/scenarios/max_batch_age_scenario.dart b/features/fixture_resources/lib/scenarios/max_batch_age_scenario.dart index 4ecc680..9db1e24 100644 --- a/features/fixture_resources/lib/scenarios/max_batch_age_scenario.dart +++ b/features/fixture_resources/lib/scenarios/max_batch_age_scenario.dart @@ -1,4 +1,3 @@ -import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'scenario.dart'; class MaxBatchAgeScenario extends Scenario { diff --git a/features/fixture_resources/lib/scenarios/probability_expiry_scenario.dart b/features/fixture_resources/lib/scenarios/probability_expiry_scenario.dart index f38996d..228cd8c 100644 --- a/features/fixture_resources/lib/scenarios/probability_expiry_scenario.dart +++ b/features/fixture_resources/lib/scenarios/probability_expiry_scenario.dart @@ -1,10 +1,7 @@ -import 'dart:convert'; - import 'package:bugsnag_flutter_performance/bugsnag_flutter_performance.dart'; import 'package:mazerunner/main.dart'; import 'scenario.dart'; -import 'package:http/http.dart' as http; class ProbabilityExpiryScenario extends Scenario { @override @@ -14,9 +11,9 @@ class ProbabilityExpiryScenario extends Scenario { bugsnag_performance.setExtraConfig("probabilityValueExpireTime", 100); bugsnag_performance.start( apiKey: '12312312312312312312312312312312', - endpoint: Uri.parse(FixtureConfig.MAZE_HOST.toString() + '/traces')); + endpoint: Uri.parse('${FixtureConfig.MAZE_HOST}/traces')); setMaxBatchSize(1); - await Future.delayed(Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); doSimpleSpan('myspan'); } } diff --git a/features/fixture_resources/lib/scenarios/scenario.dart b/features/fixture_resources/lib/scenarios/scenario.dart index bc0c3d2..c8406e4 100644 --- a/features/fixture_resources/lib/scenarios/scenario.dart +++ b/features/fixture_resources/lib/scenarios/scenario.dart @@ -49,6 +49,10 @@ abstract class Scenario { String? appVersion, bool shouldUseNotifier = false, double? samplingProbability, + int? attributeCountLimit, + int? attributeStringValueLimit, + int? attributeArrayLengthLimit, + List Function(BugsnagPerformanceSpan)>? onSpanEndCallbacks, }) async { bugsnag_performance.setExtraConfig("instrumentAppStart", false); bugsnag_performance.setExtraConfig("probabilityValueExpireTime", 1000); @@ -61,6 +65,10 @@ abstract class Scenario { serviceName: serviceName, appVersion: appVersion, samplingProbability: samplingProbability, + attributeCountLimit: attributeCountLimit, + attributeStringValueLimit: attributeStringValueLimit, + attributeArrayLengthLimit: attributeArrayLengthLimit, + onSpanEndCallbacks: onSpanEndCallbacks, ); if (shouldUseNotifier && endpointConfiguration != null) { await bugsnag.start( diff --git a/features/fixture_resources/lib/scenarios/scenarios.dart b/features/fixture_resources/lib/scenarios/scenarios.dart index bb0c40f..2a69a3a 100644 --- a/features/fixture_resources/lib/scenarios/scenarios.dart +++ b/features/fixture_resources/lib/scenarios/scenarios.dart @@ -9,6 +9,7 @@ import 'package:mazerunner/scenarios/auto_instrument_view_load_nested_scenario.d import 'package:mazerunner/scenarios/correlation_null_scenario.dart'; import 'package:mazerunner/scenarios/correlation_simple_scenario.dart'; import 'package:mazerunner/scenarios/custom_service_name_scenario.dart'; +import 'package:mazerunner/scenarios/custom_span_attributes_scenario.dart'; import 'package:mazerunner/scenarios/dart_io_traceparent_scenario.dart'; import 'package:mazerunner/scenarios/fixed_sampling_probability_one_scenario.dart'; import 'package:mazerunner/scenarios/fixed_sampling_probability_zero_scenario.dart'; @@ -17,6 +18,7 @@ import 'package:mazerunner/scenarios/http_client_traceparent_scenario.dart'; import 'auto_instrument_app_starts_scenario.dart'; import 'auto_instrument_navigation_with_view_load_scenario.dart'; +import 'custom_span_attributes_with_limits_scenario.dart'; import 'dio_callback_cancel_span.dart'; import 'dio_callback_edit_scenario.dart'; import 'initial_p_scenario.dart'; @@ -127,4 +129,8 @@ final List> scenarios = [ ScenarioInfo('FixedSamplingProbabilityZeroScenario', () => FixedSamplingProbabilityZeroScenario()), ScenarioInfo('CustomServiceNameScenario', () => CustomServiceNameScenario()), + ScenarioInfo( + 'CustomSpanAttributesScenario', () => CustomSpanAttributesScenario()), + ScenarioInfo('CustomSpanAttributesWithLimitsScenario', + () => CustomSpanAttributesWithLimitsScenario()) ]; diff --git a/features/manual_span.feature b/features/manual_span.feature index 25c1f40..a436c85 100644 --- a/features/manual_span.feature +++ b/features/manual_span.feature @@ -51,7 +51,7 @@ Feature: Manual Spans * the span named "no-parent" exists * the span named "no-parent" has no parent -Scenario: Manual Navigation Span + Scenario: Manual Navigation Span When I run "ManualNavigationSpanScenario" And I wait for 1 span Then the trace "Content-Type" header equals "application/json" @@ -69,4 +69,60 @@ Scenario: Manual Navigation Span * every span string attribute "bugsnag.navigation.previous_route" equals "navigationScenarioPreviousRoute" * a span double attribute "bugsnag.sampling.p" equals 1.0 + Scenario: Custom attributes + When I run "CustomSpanAttributesScenario" + And I wait for 1 span + Then the trace "Content-Type" header equals "application/json" + * the trace "Bugsnag-Sent-At" header matches the regex "^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ$" + * the trace "Bugsnag-Span-Sampling" header equals "1:1" + * every span field "name" equals "CustomSpanAttributesScenarioSpan" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "droppedAttributesCount" does not exist + * every span bool attribute "bugsnag.span.first_class" is true + * every span string attribute "bugsnag.span.category" equals "custom" + * a span double attribute "bugsnag.sampling.p" equals 1.0 + * a span string attribute "customAttribute1" equals "C" + * every span string attribute "customAttribute2" does not exist + * a span integer attribute "customAttribute3" equals 3 + * a span double attribute "customAttribute4" equals 42.0 + * a span array attribute "customAttribute5" contains 4 items + * a span array attribute "customAttribute5" contains the string value "customString" at index 0 + * a span array attribute "customAttribute5" contains the integer value 42 at index 1 + * a span array attribute "customAttribute5" contains the value true at index 2 + * a span array attribute "customAttribute5" contains the double value 43.0 at index 3 + * every span string attribute "customAttribute6" does not exist + + Scenario: Custom attributes - limits + When I run "CustomSpanAttributesWithLimitsScenario" + And I wait for 1 span + Then the trace "Content-Type" header equals "application/json" + * the trace "Bugsnag-Sent-At" header matches the regex "^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ$" + * the trace "Bugsnag-Span-Sampling" header equals "1:1" + * every span field "name" equals "CustomSpanAttributesWithLimitsScenarioSpan" + * every span field "spanId" matches the regex "^[A-Fa-f0-9]{16}$" + * every span field "traceId" matches the regex "^[A-Fa-f0-9]{32}$" + * every span field "startTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "endTimeUnixNano" matches the regex "^[0-9]+$" + * every span field "droppedAttributesCount" equals 3 + * every span bool attribute "bugsnag.span.first_class" is true + * every span string attribute "bugsnag.span.category" equals "custom" + * a span double attribute "bugsnag.sampling.p" equals 1.0 + * a span integer attribute "customAttribute1" equals 42 + * every span string attribute "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" does not exist + * a span string attribute "customAttribute2" equals "NotDropped" + * a span array attribute "customAttribute3" contains 6 items + * a span array attribute "customAttribute3" contains the integer value 1 at index 0 + * a span array attribute "customAttribute3" contains the integer value 2 at index 1 + * a span array attribute "customAttribute3" contains the integer value 3 at index 2 + * a span array attribute "customAttribute3" contains the integer value 4 at index 3 + * a span array attribute "customAttribute3" contains the integer value 5 at index 4 + * a span array attribute "customAttribute3" contains the integer value 6 at index 5 + * a span string attribute "customAttribute4" equals "VeryLongStringAttrib*** 27 CHARS TRUNCATED" + * a span double attribute "customAttribute5" equals 42.0 + * every span string attribute "customAttribute6" does not exist + * every span string attribute "customAttribute7" does not exist + diff --git a/features/steps/flutter_steps.rb b/features/steps/flutter_steps.rb index e7f2b51..221825e 100644 --- a/features/steps/flutter_steps.rb +++ b/features/steps/flutter_steps.rb @@ -123,6 +123,13 @@ def execute_command(action, scenario_name) Maze.check.false(attribute_values.empty?) end +Then('a span integer attribute {string} equals {int}') do |attribute, expected| + spans = spans_from_request_list(Maze::Server.list_for('traces')) + selected_attributes = spans.map { |span| span['attributes'].find { |a| a['key'].eql?(attribute) && a['value'].has_key?('intValue') } }.compact + attribute_values = selected_attributes.map { |a| a['value']['intValue'].to_i == expected } + Maze.check.false(attribute_values.empty?) +end + Then('a span double attribute {string} equals {float}') do |attribute, value| spans = spans_from_request_list(Maze::Server.list_for('traces')) selected_attributes = spans.map { |span| span['attributes'].find { |a| a['key'].eql?(attribute) && a['value'].has_key?('doubleValue') } }.compact @@ -201,3 +208,67 @@ def execute_command(action, scenario_name) Maze.check.true(spans_with_name.length() == 0); end + +Then('every span field {string} does not exist') do |key| + spans = spans_from_request_list(Maze::Server.list_for('traces')) + spans.map { |span| Maze.check.nil span[key] } +end + +Then('every span field {string} equals {int}') do |key, expected| + spans = spans_from_request_list(Maze::Server.list_for('traces')) + selected_keys = spans.map { |span| span[key] == expected } + Maze.check.not_includes selected_keys, false +end + +Then('a span array attribute {string} contains the string value {string} at index {int}') do |attribute, expected, index| + value = get_array_value_at_index(attribute, index, 'stringValue') + Maze.check.true(value == expected) +end + +Then('a span array attribute {string} contains the integer value {int} at index {int}') do |attribute, expected, index| + value = get_array_value_at_index(attribute, index, 'intValue') + Maze.check.true(value.to_i == expected) +end + +Then('a span array attribute {string} contains the double value {float} at index {int}') do |attribute, expected, index| + value = get_array_value_at_index(attribute, index, 'doubleValue') + Maze.check.true(value == expected) +end + +Then('a span array attribute {string} contains the value true at index {int}') do |attribute, index| + value = get_array_value_at_index(attribute, index, 'boolValue') + Maze.check.true(value == true) +end + +Then('a span array attribute {string} contains the value false at index {int}') do |attribute, index| + value = get_array_value_at_index(attribute, index, 'boolValue') + Maze.check.true(value == false) +end + +Then('a span array attribute {string} contains {int} items') do |attribute, length| + array = get_array_attribute_contents(attribute) + Maze.check.true(array.length() == length) +end + +Then('a span array attribute {string} is empty') do |attribute| + array_contents = get_array_attribute_contents(attribute) + Maze.check.true(array_contents.empty?) +end + +def get_array_value_at_index(attribute, index, type) + array = get_array_attribute_contents(attribute) + Maze.check.true(array.length() > index) + value = array[index] + Maze.check.true(value.has_key?(type)) + return value[type] +end + +def get_array_attribute_contents(attribute) + spans = spans_from_request_list(Maze::Server.list_for('traces')) + selected_attributes = spans.map { |span| span['attributes'].find { |a| a['key'].eql?(attribute) && + a['value'].has_key?('arrayValue') && + a['value']['arrayValue'].has_key?('values') } }.compact + array_attributes = selected_attributes.map { |a| a['value']['arrayValue']['values'] } + Maze.check.false(array_attributes.empty?) + return array_attributes[0] +end diff --git a/features/support/maze.buildkite.cfg b/features/support/maze.buildkite.cfg new file mode 100644 index 0000000..c5bc182 --- /dev/null +++ b/features/support/maze.buildkite.cfg @@ -0,0 +1,3 @@ +--format=junit +--out=reports +--format=pretty diff --git a/packages/bugsnag_flutter_performance/lib/bugsnag_flutter_performance.dart b/packages/bugsnag_flutter_performance/lib/bugsnag_flutter_performance.dart index f24e91e..c630d14 100644 --- a/packages/bugsnag_flutter_performance/lib/bugsnag_flutter_performance.dart +++ b/packages/bugsnag_flutter_performance/lib/bugsnag_flutter_performance.dart @@ -44,6 +44,10 @@ class BugsnagPerformance { String? serviceName, String? appVersion, double? samplingProbability, + int? attributeCountLimit, + int? attributeStringValueLimit, + int? attributeArrayLengthLimit, + List Function(BugsnagPerformanceSpan)>? onSpanEndCallbacks, }) { _validateApiKey(apiKey); return _client.start( @@ -56,6 +60,10 @@ class BugsnagPerformance { serviceName: serviceName, appVersion: appVersion, samplingProbability: samplingProbability, + attributeCountLimit: attributeCountLimit, + attributeStringValueLimit: attributeStringValueLimit, + attributeArrayLengthLimit: attributeArrayLengthLimit, + onSpanEndCallbacks: onSpanEndCallbacks, ); } diff --git a/packages/bugsnag_flutter_performance/lib/src/client.dart b/packages/bugsnag_flutter_performance/lib/src/client.dart index 7eb3260..98cea80 100644 --- a/packages/bugsnag_flutter_performance/lib/src/client.dart +++ b/packages/bugsnag_flutter_performance/lib/src/client.dart @@ -10,6 +10,7 @@ import 'package:bugsnag_flutter_performance/src/instrumentation/navigation/navig import 'package:bugsnag_flutter_performance/src/instrumentation/view_load/measured_widget_callbacks.dart'; import 'package:bugsnag_flutter_performance/src/instrumentation/view_load/view_load_instrumentation.dart'; import 'package:bugsnag_flutter_performance/src/span_attributes.dart'; +import 'package:bugsnag_flutter_performance/src/span_attributes_limits.dart'; import 'package:bugsnag_flutter_performance/src/span_context.dart'; import 'package:bugsnag_flutter_performance/src/uploader/package_builder.dart'; import 'package:bugsnag_flutter_performance/src/uploader/retry_queue.dart'; @@ -27,6 +28,8 @@ import 'bugsnag_network_request_info.dart'; import 'configuration.dart'; import 'span.dart'; +typedef OnSpanEndCallback = Future Function(BugsnagPerformanceSpan); + String _defaultEndpoint(String? apiKey) => 'https://${apiKey != null ? '$apiKey.' : ''}otlp.bugsnag.com/v1/traces'; @@ -40,6 +43,8 @@ abstract class BugsnagPerformanceClient { List? enabledReleaseStages, String? serviceName, String? appVersion, + double? samplingProbability, + List? onSpanEndCallbacks, }); Future measureRunApp(FutureOr Function() runApp); @@ -89,6 +94,7 @@ class BugsnagPerformanceClientImpl implements BugsnagPerformanceClient { RetryQueue? _retryQueue; Sampler? _sampler; DateTime? _lastSamplingProbabilityRefreshDate; + List _onSpanEndCallbacks = []; late final PackageBuilder _packageBuilder; late final BugsnagClock _clock; late final BugsnagLifecycleListener? _lifecycleListener; @@ -136,6 +142,10 @@ class BugsnagPerformanceClientImpl implements BugsnagPerformanceClient { String? serviceName, String? appVersion, double? samplingProbability, + int? attributeCountLimit, + int? attributeStringValueLimit, + int? attributeArrayLengthLimit, + List? onSpanEndCallbacks, }) async { if (!_isEnabledOnCurrentPlatform()) { _appStartInstrumentation.setEnabled(false); @@ -144,6 +154,12 @@ class BugsnagPerformanceClientImpl implements BugsnagPerformanceClient { } WidgetsFlutterBinding.ensureInitialized(); _networkRequestCallback = networkRequestCallback; + _onSpanEndCallbacks = onSpanEndCallbacks ?? []; + BugsnagPerformanceSpanImpl.globalAttributeCountLimit = + SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.attributeCountLimit, + providedValue: attributeCountLimit, + ); configuration = BugsnagPerformanceConfiguration( apiKey: apiKey, endpoint: endpoint ?? Uri.parse(_defaultEndpoint(apiKey)), @@ -153,7 +169,21 @@ class BugsnagPerformanceClientImpl implements BugsnagPerformanceClient { serviceName: serviceName, appVersion: appVersion, samplingProbability: samplingProbability, + attributeCountLimit: SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.attributeCountLimit, + providedValue: attributeCountLimit, + ), + attributeStringValueLimit: SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.stringValueLimit, + providedValue: attributeStringValueLimit, + ), + attributeArrayLengthLimit: SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.arrayLengthLimit, + providedValue: attributeArrayLengthLimit, + ), ); + BugsnagPerformanceSpanImpl.globalAttributeCountLimit = + configuration!.attributeCountLimit; _packageBuilder.setConfig(configuration); _initialExtraConfig.forEach((key, value) { setExtraConfig(key, value); @@ -200,7 +230,8 @@ class BugsnagPerformanceClientImpl implements BugsnagPerformanceClient { startTime: startTime ?? _clock.now(), onEnded: (endedSpan) async { await _updateSamplingProbabilityIfNeeded(); - if (await _sampler?.sample(endedSpan) ?? true) { + if ((await _sampler?.sample(endedSpan) ?? true) && + (await _callOnSpanEndedCallbacks(endedSpan))) { _currentBatch?.add(endedSpan); } _potentiallyOpenSpans.remove(endedSpan.spanId); @@ -211,6 +242,7 @@ class BugsnagPerformanceClientImpl implements BugsnagPerformanceClient { parentSpanId: parent?.spanId, traceId: parent?.traceId, attributes: attributes, + attributeCountLimit: configuration?.attributeCountLimit, ); span.clock = _clock; if (configuration != null) { @@ -540,4 +572,28 @@ class BugsnagPerformanceClientImpl implements BugsnagPerformanceClient { bool _isEnabledOnCurrentPlatform() { return !kIsWeb; } + + Future _callOnSpanEndedCallbacks(BugsnagPerformanceSpan span) async { + if (span is BugsnagPerformanceSpanImpl) { + span.makeMutable(true); + } + try { + for (OnSpanEndCallback callback in _onSpanEndCallbacks) { + try { + if (!(await callback(span))) { + return false; + } + } catch (e) { + if (kDebugMode) { + print('onSpanEnd callback threw exception: $e'); + } + } + } + } finally { + if (span is BugsnagPerformanceSpanImpl) { + span.makeMutable(false); + } + } + return true; + } } diff --git a/packages/bugsnag_flutter_performance/lib/src/configuration.dart b/packages/bugsnag_flutter_performance/lib/src/configuration.dart index 73103d4..7e963b4 100644 --- a/packages/bugsnag_flutter_performance/lib/src/configuration.dart +++ b/packages/bugsnag_flutter_performance/lib/src/configuration.dart @@ -8,6 +8,9 @@ class BugsnagPerformanceConfiguration { this.serviceName, this.appVersion, this.samplingProbability, + required this.attributeCountLimit, + required this.attributeStringValueLimit, + required this.attributeArrayLengthLimit, }); String? apiKey; Uri? endpoint; @@ -24,6 +27,9 @@ class BugsnagPerformanceConfiguration { String? serviceName; String? appVersion; double? samplingProbability; + int attributeCountLimit; + int attributeStringValueLimit; + int attributeArrayLengthLimit; bool releaseStageEnabled() { return releaseStage == null || diff --git a/packages/bugsnag_flutter_performance/lib/src/extensions/resource_attributes.dart b/packages/bugsnag_flutter_performance/lib/src/extensions/resource_attributes.dart index 6b9146a..d94ba2d 100644 --- a/packages/bugsnag_flutter_performance/lib/src/extensions/resource_attributes.dart +++ b/packages/bugsnag_flutter_performance/lib/src/extensions/resource_attributes.dart @@ -196,5 +196,5 @@ class ResourceAttributesProviderImpl implements ResourceAttributesProvider { return "Unknown"; } - static String get _getSDKVersion => '1.2.1'; + static String get _getSDKVersion => '1.3.0'; } diff --git a/packages/bugsnag_flutter_performance/lib/src/span.dart b/packages/bugsnag_flutter_performance/lib/src/span.dart index 5e3203f..3b150a0 100644 --- a/packages/bugsnag_flutter_performance/lib/src/span.dart +++ b/packages/bugsnag_flutter_performance/lib/src/span.dart @@ -1,9 +1,12 @@ +import 'package:bugsnag_flutter_performance/src/configuration.dart'; import 'package:bugsnag_flutter_performance/src/extensions/date_time.dart'; import 'package:bugsnag_flutter_performance/src/extensions/int.dart'; import 'package:bugsnag_flutter_performance/src/span_attributes.dart'; +import 'package:bugsnag_flutter_performance/src/span_attributes_limits.dart'; import 'package:bugsnag_flutter_performance/src/span_context.dart'; import 'package:bugsnag_flutter_performance/src/util/clock.dart'; import 'package:bugsnag_flutter_performance/src/util/random.dart'; +import 'package:flutter/foundation.dart'; typedef TraceId = BigInt; typedef SpanId = BigInt; @@ -23,7 +26,13 @@ abstract class BugsnagPerformanceSpan implements BugsnagPerformanceSpanContext { }); String get encodedTraceId; String get encodedSpanId; - dynamic toJson(); + String get name; + DateTime get startTime; + DateTime? get endTime; + void setAttribute(String key, dynamic value); + dynamic toJson({ + BugsnagPerformanceConfiguration? config, + }); } class BugsnagPerformanceSpanImpl @@ -36,13 +45,20 @@ class BugsnagPerformanceSpanImpl TraceId? traceId, SpanId? spanId, this.parentSpanId, + int? attributeCountLimit, BugsnagPerformanceSpanAttributes? attributes}) { this.traceId = traceId ?? randomTraceId(); this.spanId = spanId ?? randomSpanId(); this.onEnded = onEnded ?? _onEnded; this.onCanceled = onCanceled ?? _onCanceled; + this.attributeCountLimit = attributeCountLimit ?? globalAttributeCountLimit; this.attributes = attributes ?? BugsnagPerformanceSpanAttributes(); } + + static int globalAttributeCountLimit = SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.attributeCountLimit); + + @override final String name; @override late final TraceId traceId; @@ -50,13 +66,19 @@ class BugsnagPerformanceSpanImpl late final SpanId spanId; @override SpanId? parentSpanId; + @override final DateTime startTime; late final BugsnagPerformanceSpanAttributes attributes; - DateTime? endTime; + DateTime? _endTime; var isSampled = false; + var _isMutable = true; late final void Function(BugsnagPerformanceSpan) onEnded; late final void Function(BugsnagPerformanceSpan) onCanceled; late final BugsnagClock clock; + late final int attributeCountLimit; + @override + DateTime? get endTime => _endTime; + var _droppedAttributesCountBeforeEncoding = 0; @override void end({ @@ -69,7 +91,8 @@ class BugsnagPerformanceSpanImpl if (!isOpen()) { return; } - this.endTime = endTime ?? clock.now(); + _endTime = endTime ?? clock.now(); + makeMutable(false); if (cancelled) { onCanceled(this); return; @@ -85,11 +108,34 @@ class BugsnagPerformanceSpanImpl onEnded(this); } + @override + void setAttribute(String key, dynamic value) { + if (!_isMutable) { + if (kDebugMode) { + print( + 'Span attribute "$key" in span $name was dropped as the span is no longer open'); + } + return; + } + if (!attributes.hasAttribute(key) && + value != null && + attributes.count >= attributeCountLimit) { + _droppedAttributesCountBeforeEncoding++; + if (kDebugMode) { + print( + 'Span attribute "$key" in span $name was dropped as the number of attributes exceeds the $attributeCountLimit attribute limit set by AttributeCountLimit.'); + } + return; + } + + attributes.setAttribute(key, value); + } + BugsnagPerformanceSpanImpl.fromJson(Map json, [void Function(BugsnagPerformanceSpan)? onEnded]) : startTime = int.parse(json['startTimeUnixNano']).timeFromNanos, name = json['name'] as String, - endTime = json['endTimeUnixNano'] != null + _endTime = json['endTimeUnixNano'] != null ? int.parse(json['endTimeUnixNano']).timeFromNanos : null, traceId = _decodeTraceId(json['traceId'] as String?) ?? randomTraceId(), @@ -100,18 +146,27 @@ class BugsnagPerformanceSpanImpl BugsnagPerformanceSpanAttributes.fromJson(json['attributes']); @override - dynamic toJson() => { - 'startTimeUnixNano': startTime.nanosecondsSinceEpoch.toString(), - 'name': name, - if (endTime != null) - 'endTimeUnixNano': endTime!.nanosecondsSinceEpoch.toString(), - 'traceId': encodedTraceId, - 'spanId': encodedSpanId, - 'kind': 1, - if (parentSpanId != null) - 'parentSpanId': _encodeSpanId(parentSpanId ?? BigInt.zero), - 'attributes': attributes.toJson(), - }; + dynamic toJson({ + BugsnagPerformanceConfiguration? config, + }) { + final attributesEncodingResult = attributes.toJson(config: config); + final droppedAttributesCount = _droppedAttributesCountBeforeEncoding + + attributesEncodingResult.droppedAttributesCount; + return { + 'startTimeUnixNano': startTime.nanosecondsSinceEpoch.toString(), + 'name': name, + if (_endTime != null) + 'endTimeUnixNano': _endTime!.nanosecondsSinceEpoch.toString(), + 'traceId': encodedTraceId, + 'spanId': encodedSpanId, + 'kind': 1, + if (parentSpanId != null) + 'parentSpanId': _encodeSpanId(parentSpanId ?? BigInt.zero), + 'attributes': attributesEncodingResult.jsonValue, + if (droppedAttributesCount > 0) + 'droppedAttributesCount': droppedAttributesCount, + }; + } @override bool operator ==(Object other) => @@ -132,7 +187,7 @@ class BugsnagPerformanceSpanImpl @override bool isOpen() { - return endTime == null; + return _endTime == null; } @override @@ -140,6 +195,10 @@ class BugsnagPerformanceSpanImpl @override String get encodedSpanId => _encodeSpanId(spanId); + + void makeMutable(bool mutable) { + _isMutable = mutable; + } } String _encodeSpanId(SpanId spanId) { diff --git a/packages/bugsnag_flutter_performance/lib/src/span_attributes.dart b/packages/bugsnag_flutter_performance/lib/src/span_attributes.dart index 5330d5e..b031e43 100644 --- a/packages/bugsnag_flutter_performance/lib/src/span_attributes.dart +++ b/packages/bugsnag_flutter_performance/lib/src/span_attributes.dart @@ -1,3 +1,17 @@ +import 'package:bugsnag_flutter_performance/src/configuration.dart'; +import 'package:bugsnag_flutter_performance/src/span_attributes_limits.dart'; +import 'package:flutter/foundation.dart'; + +class BugsnagPerformanceSpanAttributesEncodingResult { + final dynamic jsonValue; + final int droppedAttributesCount; + + BugsnagPerformanceSpanAttributesEncodingResult({ + required this.jsonValue, + required this.droppedAttributesCount, + }); +} + class BugsnagPerformanceSpanAttributes { final Map attributes = {}; @@ -26,6 +40,8 @@ class BugsnagPerformanceSpanAttributes { void setAttribute(String key, dynamic value) { if (value != null) { attributes[key] = value; + } else { + attributes.remove(key); } } @@ -49,44 +65,37 @@ class BugsnagPerformanceSpanAttributes { return attributes['bugsnag.sampling.p']; } - dynamic toJson() { - const typeMap = { - bool: 'boolValue', - double: 'doubleValue', - int: 'intValue', - String: 'stringValue', - }; + int get count => attributes.length; - return attributes.entries.map((entry) { - final key = entry.key; - final value = entry.value; - String valueType = typeMap[value.runtimeType] ?? 'stringValue'; - dynamic formattedValue = value is int ? value.toString() : value; + bool hasAttribute(String key) { + return attributes.containsKey(key); + } - return { - 'key': key, - 'value': {valueType: formattedValue}, - }; - }).toList(); + BugsnagPerformanceSpanAttributesEncodingResult toJson({ + BugsnagPerformanceConfiguration? config, + }) { + final attributeKeyLengthLimit = SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.keyLengthLimit); + final attributeStringValueLimit = config?.attributeStringValueLimit ?? + SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.stringValueLimit); + final attributeArrayLengthLimit = config?.attributeArrayLengthLimit ?? + SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.arrayLengthLimit); + + return BugsnagPerformanceSpanAttributesEncoder( + attributeKeyLengthLimit: attributeKeyLengthLimit, + attributeStringValueLimit: attributeStringValueLimit, + attributeArrayLengthLimit: attributeArrayLengthLimit, + ).toJson(attributes); } factory BugsnagPerformanceSpanAttributes.fromJson( List> json) { final attributes = {}; - for (var element in json) { + for (final element in json) { final key = element['key']; - final Map valueMap = element['value']; - dynamic value; - - if (valueMap.containsKey('stringValue')) { - value = valueMap['stringValue']; - } else if (valueMap.containsKey('boolValue')) { - value = valueMap['boolValue']; - } else if (valueMap.containsKey('doubleValue')) { - value = double.tryParse(valueMap['doubleValue'].toString()); - } else if (valueMap.containsKey('intValue')) { - value = int.tryParse(valueMap['intValue']); - } + dynamic value = _decodeAttributeValue(element['value']); if (key != null && value != null) { attributes[key] = value; @@ -94,4 +103,140 @@ class BugsnagPerformanceSpanAttributes { } return BugsnagPerformanceSpanAttributes(additionalAttributes: attributes); } + + static dynamic _decodeAttributeValue(Map valueMap) { + if (valueMap.containsKey('stringValue')) { + return valueMap['stringValue']; + } else if (valueMap.containsKey('boolValue')) { + return valueMap['boolValue']; + } else if (valueMap.containsKey('doubleValue')) { + return double.tryParse(valueMap['doubleValue'].toString()); + } else if (valueMap.containsKey('intValue')) { + return int.tryParse(valueMap['intValue']); + } else if (valueMap.containsKey('arrayValue')) { + final arrayValue = valueMap['arrayValue']; + if (arrayValue is Map && arrayValue.containsKey('values')) { + List result = []; + for (final element in arrayValue['values']!) { + dynamic decodedValue = _decodeAttributeValue(element); + if (decodedValue != null) { + result.add(decodedValue); + } + } + return result; + } + } + return null; + } +} + +class BugsnagPerformanceSpanAttributesEncoder { + int attributeKeyLengthLimit; + int attributeStringValueLimit; + int attributeArrayLengthLimit; + + BugsnagPerformanceSpanAttributesEncoder({ + required this.attributeKeyLengthLimit, + required this.attributeStringValueLimit, + required this.attributeArrayLengthLimit, + }); + + static const _typeMap = { + bool: 'boolValue', + double: 'doubleValue', + int: 'intValue', + String: 'stringValue' + }; + + static const _listTypeMap = { + List: 'arrayValue', + List: 'arrayValue', + List: 'arrayValue', + List: 'arrayValue', + List: 'arrayValue' + }; + + BugsnagPerformanceSpanAttributesEncodingResult toJson( + Map attributes, + ) { + final validKeys = + attributes.keys.where((key) => key.length <= attributeKeyLengthLimit); + + final jsonValue = validKeys.map((key) { + final value = attributes[key]; + + return { + 'key': key, + 'value': _encodeAttributeValue(key, value), + }; + }).toList(); + + return BugsnagPerformanceSpanAttributesEncodingResult( + jsonValue: jsonValue, + droppedAttributesCount: attributes.length - validKeys.length, + ); + } + + dynamic _encodeAttributeValue( + String key, + dynamic value, + ) { + final runtimeType = value.runtimeType; + String valueType = + _typeMap[runtimeType] ?? _listTypeMap[runtimeType] ?? 'stringValue'; + dynamic formattedValue = _formattedValue(key, value); + return {valueType: formattedValue}; + } + + dynamic _formattedValue( + String key, + dynamic value, + ) { + if (value is String) { + if (value.length > attributeStringValueLimit) { + if (kDebugMode) { + print( + 'The value for span attribute "$key" was truncated as it exceeds the $attributeStringValueLimit attribute limit set by AttributeStringValueLimit'); + } + return '${value.substring(0, attributeStringValueLimit)}*** ${value.length - attributeStringValueLimit} CHARS TRUNCATED'; + } + return value; + } + if (value is int) { + return value.toString(); + } + if (value is List) { + return _encodeListAttributeValue(key, value); + } + if (value is double || value is bool) { + return value; + } + return value.toString(); + } + + dynamic _encodeListAttributeValue( + String key, + List listAttribute, + ) { + List result = []; + for (final (index, value) in listAttribute.indexed) { + final runtimeType = value.runtimeType; + if (_typeMap[runtimeType] != null) { + result.add(_encodeAttributeValue(key, value)); + } else { + if (kDebugMode) { + print( + 'The element at index $index for span attribute "$key" was excluded as its type is not valid.'); + } + } + } + if (result.length > attributeArrayLengthLimit) { + if (kDebugMode) { + print( + 'The value for span attribute "$key" was truncated as it exceeds the $attributeArrayLengthLimit attribute limit set by AttributeArrayLengthLimit'); + } + result = result.sublist(0, attributeArrayLengthLimit).toList(); + } + return {'values': result}; + } } diff --git a/packages/bugsnag_flutter_performance/lib/src/span_attributes_limits.dart b/packages/bugsnag_flutter_performance/lib/src/span_attributes_limits.dart new file mode 100644 index 0000000..aa8fdd7 --- /dev/null +++ b/packages/bugsnag_flutter_performance/lib/src/span_attributes_limits.dart @@ -0,0 +1,77 @@ +enum SpanAttributesLimitType { + keyLengthLimit, + stringValueLimit, + arrayLengthLimit, + attributeCountLimit, +} + +class SpanAttributesLimits { + static const _attributeKeyLengthLimit = 128; + + static const _defaultAttributeStringValueLimit = 1024; + static const _minAttributeStringValueLimit = 1; + static const _maxAttributeStringValueLimit = 10000; + + static const _defaultAttributeArrayLengthLimit = 1000; + static const _minAttributeArrayLengthLimit = 1; + static const _maxAttributeArrayLengthLimit = 10000; + + static const _defaultAttributeCountLimit = 128; + static const _minAttributeCountLimit = 1; + static const _maxAttributeCountLimit = 1000; + + static int limitValue({ + required SpanAttributesLimitType type, + int? providedValue, + }) { + if (providedValue == null) { + return _defaultValue(type); + } + if (providedValue < _minValue(type)) { + return _defaultValue(type); + } + if (providedValue > _maxValue(type)) { + return _maxValue(type); + } + return providedValue; + } + + static int _defaultValue(SpanAttributesLimitType type) { + switch (type) { + case SpanAttributesLimitType.keyLengthLimit: + return _attributeKeyLengthLimit; + case SpanAttributesLimitType.stringValueLimit: + return _defaultAttributeStringValueLimit; + case SpanAttributesLimitType.arrayLengthLimit: + return _defaultAttributeArrayLengthLimit; + case SpanAttributesLimitType.attributeCountLimit: + return _defaultAttributeCountLimit; + } + } + + static int _minValue(SpanAttributesLimitType type) { + switch (type) { + case SpanAttributesLimitType.keyLengthLimit: + return _attributeKeyLengthLimit; + case SpanAttributesLimitType.stringValueLimit: + return _minAttributeStringValueLimit; + case SpanAttributesLimitType.arrayLengthLimit: + return _minAttributeArrayLengthLimit; + case SpanAttributesLimitType.attributeCountLimit: + return _minAttributeCountLimit; + } + } + + static int _maxValue(SpanAttributesLimitType type) { + switch (type) { + case SpanAttributesLimitType.keyLengthLimit: + return _attributeKeyLengthLimit; + case SpanAttributesLimitType.stringValueLimit: + return _maxAttributeStringValueLimit; + case SpanAttributesLimitType.arrayLengthLimit: + return _maxAttributeArrayLengthLimit; + case SpanAttributesLimitType.attributeCountLimit: + return _maxAttributeCountLimit; + } + } +} diff --git a/packages/bugsnag_flutter_performance/lib/src/uploader/package_builder.dart b/packages/bugsnag_flutter_performance/lib/src/uploader/package_builder.dart index 2d5d259..63e8274 100644 --- a/packages/bugsnag_flutter_performance/lib/src/uploader/package_builder.dart +++ b/packages/bugsnag_flutter_performance/lib/src/uploader/package_builder.dart @@ -61,7 +61,7 @@ class PackageBuilderImpl implements PackageBuilder { Future> _buildPayload({ required List spans, }) async { - final jsonList = spans.map((span) => span.toJson()).toList(); + final jsonList = spans.map((span) => span.toJson(config: _config)).toList(); final jsonRequest = { 'resourceSpans': [ { diff --git a/packages/bugsnag_flutter_performance/lib/src/uploader/retry_queue.dart b/packages/bugsnag_flutter_performance/lib/src/uploader/retry_queue.dart index 4881ca6..b4d70d2 100644 --- a/packages/bugsnag_flutter_performance/lib/src/uploader/retry_queue.dart +++ b/packages/bugsnag_flutter_performance/lib/src/uploader/retry_queue.dart @@ -44,8 +44,11 @@ class FileRetryQueue implements RetryQueue { final Uploader? _uploader; var _isFlushing = false; + final Directory? _cacheDirectory; - FileRetryQueue(Uploader uploader) : _uploader = uploader; + FileRetryQueue(Uploader uploader, {Directory? cacheDirectory}) + : _uploader = uploader, + _cacheDirectory = cacheDirectory; @override Future enqueue({ @@ -60,7 +63,7 @@ class FileRetryQueue implements RetryQueue { @override Future flush() async { - final cacheDirectory = await _getCacheDirectory(); + final cacheDirectory = _cacheDirectory ?? await _getCacheDirectory(); if (_isFlushing || !cacheDirectory.existsSync()) { return; } diff --git a/packages/bugsnag_flutter_performance/pubspec.yaml b/packages/bugsnag_flutter_performance/pubspec.yaml index 6446555..ceece08 100644 --- a/packages/bugsnag_flutter_performance/pubspec.yaml +++ b/packages/bugsnag_flutter_performance/pubspec.yaml @@ -1,6 +1,6 @@ name: bugsnag_flutter_performance description: BugSnag performance monitoring tool for Flutter apps -version: 1.2.1 +version: 1.3.0 homepage: https://www.bugsnag.com/ documentation: https://docs.bugsnag.com/performance/flutter/ repository: https://github.com/bugsnag/bugsnag-flutter-performance @@ -26,6 +26,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 + path_provider_platform_interface: ^2.0.4 flutter: # This section identifies this Flutter project as a plugin project. diff --git a/packages/bugsnag_flutter_performance/test/src/span_test.dart b/packages/bugsnag_flutter_performance/test/src/span_test.dart index 8db1a5f..ca70a9d 100644 --- a/packages/bugsnag_flutter_performance/test/src/span_test.dart +++ b/packages/bugsnag_flutter_performance/test/src/span_test.dart @@ -1,3 +1,4 @@ +import 'package:bugsnag_flutter_performance/src/configuration.dart'; import 'package:bugsnag_flutter_performance/src/extensions/bugsnag_lifecycle_listener.dart'; import 'package:bugsnag_flutter_performance/src/extensions/date_time.dart'; import 'package:bugsnag_flutter_performance/src/extensions/int.dart'; @@ -127,6 +128,19 @@ void main() { 'key': 'custom', 'value': {'stringValue': 'value'} }, + { + 'key': 'customArray', + 'value': { + 'arrayValue': { + 'values': [ + {'stringValue': 'testValue'}, + {'intValue': '1'}, + {'doubleValue': 4.2}, + {'boolValue': true}, + ] + } + } + }, ], }; final span = BugsnagPerformanceSpanImpl.fromJson(json); @@ -147,6 +161,14 @@ void main() { span.parentSpanId, equals( BigInt.tryParse(json['parentSpanId'] as String, radix: 16)!)); + expect( + span.attributes.attributes['customArray'], + equals([ + 'testValue', + 1, + 4.2, + true, + ])); }); test('should decode a running span', () { @@ -185,6 +207,9 @@ void main() { parentSpanId: randomSpanId(), ); span.clock = BugsnagClockImpl.instance; + span.setAttribute('customString', 'testValue'); + span.setAttribute('customInt', 2); + span.setAttribute('list', [42, 43.0, 'testString', false, true, 1]); span.end(); final json = span.toJson(); expect(json['name'], equals(span.name)); @@ -199,6 +224,40 @@ void main() { equals(span.spanId.toRadixString(16).padLeft(16, '0'))); expect(json['parentSpanId'], equals(span.parentSpanId!.toRadixString(16).padLeft(16, '0'))); + final attributes = json['attributes'] as List>; + expect( + attributes[0], + equals({ + 'key': 'customString', + 'value': {'stringValue': 'testValue'}, + }), + ); + expect( + attributes[1], + equals({ + 'key': 'customInt', + 'value': {'intValue': '2'}, + }), + ); + expect( + attributes[2], + equals({ + 'key': 'list', + 'value': { + 'arrayValue': { + 'values': [ + {'intValue': '42'}, + {'doubleValue': 43.0}, + {'stringValue': 'testString'}, + {'boolValue': false}, + {'boolValue': true}, + {'intValue': '1'}, + ], + } + }, + }), + ); + expect(json['droppedAttributesCount'], isNull); }); test('should encode a running span', () { @@ -212,6 +271,117 @@ void main() { expect(int.parse(json['startTimeUnixNano']), equals(span.startTime.nanosecondsSinceEpoch)); expect(json['endTimeUnixNano'], isNull); + expect(json['droppedAttributesCount'], isNull); + }); + + test('should drop attributes with too long keys', () { + const tooLongKey = + 'trberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwc'; + const tooLongKey2 = + 'Atrbesfsdffrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwc'; + final span = BugsnagPerformanceSpanImpl( + name: 'Test name', + startTime: DateTime.fromMillisecondsSinceEpoch( + millisecondsSinceEpoch, + isUtc: true)); + span.clock = BugsnagClockImpl.instance; + span.setAttribute(tooLongKey, 'test'); + span.setAttribute(tooLongKey2, 'test2'); + span.setAttribute('TestBool', false); + span.end(); + final json = span.toJson(); + final attributes = json['attributes'] as List>; + expect(attributes.length, equals(1)); + expect( + attributes[0], + equals({ + 'key': 'TestBool', + 'value': {'boolValue': false}, + })); + expect(json['droppedAttributesCount'], equals(2)); + }); + + test( + 'should include attributes added over limit along with those with too long keys in droppedAttributesCount', + () { + const tooLongKey = + 'trberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwc'; + const tooLongKey2 = + 'Atrbesfsdffrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwctrberwfqfrefwefrgrewfrfwefwftvrvwreqwcwc'; + final span = BugsnagPerformanceSpanImpl( + name: 'Test name', + startTime: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, + isUtc: true), + attributeCountLimit: 3, + ); + span.clock = BugsnagClockImpl.instance; + span.setAttribute('TestInt', 1); + span.setAttribute(tooLongKey, 'test'); + span.setAttribute(tooLongKey2, 'test2'); + span.setAttribute('TestBool', false); + span.setAttribute('TestBool2', true); + span.setAttribute('TestInt', 2); + span.setAttribute('TestInt', 3); + span.end(); + final json = span.toJson(); + final attributes = json['attributes'] as List>; + expect(attributes.length, equals(1)); + expect( + attributes[0], + equals({ + 'key': 'TestInt', + 'value': {'intValue': '3'}, + })); + expect(json['droppedAttributesCount'], equals(4)); + }); + + test('should truncate strings and arrays that go over the limits', () { + final span = BugsnagPerformanceSpanImpl( + name: 'Test name', + startTime: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, + isUtc: true), + ); + span.clock = BugsnagClockImpl.instance; + span.setAttribute('TestString', 'test'); + span.setAttribute('LongTestString', 'This is a very long string'); + span.setAttribute('TestArray', [true, '2', 3.0, 4, '5', 6.0, 7]); + span.end(); + final config = BugsnagPerformanceConfiguration( + attributeCountLimit: 100, + attributeStringValueLimit: 10, + attributeArrayLengthLimit: 4, + ); + final json = span.toJson(config: config); + final attributes = json['attributes'] as List>; + expect(attributes.length, equals(3)); + expect( + attributes[0], + equals({ + 'key': 'TestString', + 'value': {'stringValue': 'test'}, + })); + expect( + attributes[1], + equals({ + 'key': 'LongTestString', + 'value': {'stringValue': 'This is a *** 16 CHARS TRUNCATED'}, + })); + expect( + attributes[2], + equals({ + 'key': 'TestArray', + 'value': { + 'arrayValue': { + 'values': [ + {'boolValue': true}, + {'stringValue': '2'}, + {'doubleValue': 3.0}, + {'intValue': '4'} + ], + } + }, + })); + expect(json['droppedAttributesCount'], isNull); }); }); }); diff --git a/packages/bugsnag_flutter_performance/test/src/uploader/retry_queue_test.dart b/packages/bugsnag_flutter_performance/test/src/uploader/retry_queue_test.dart new file mode 100644 index 0000000..d8df958 --- /dev/null +++ b/packages/bugsnag_flutter_performance/test/src/uploader/retry_queue_test.dart @@ -0,0 +1,123 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:bugsnag_flutter_performance/src/uploader/model/otlp_package.dart'; +import 'package:bugsnag_flutter_performance/src/util/clock.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:bugsnag_flutter_performance/src/uploader/uploader.dart'; +import 'package:bugsnag_flutter_performance/src/uploader/retry_queue.dart'; + +class TestUploader implements Uploader { + final RequestResult resultToReturn; + final bool throwError; + + TestUploader({this.resultToReturn = RequestResult.success, this.throwError = false}); + + @override + Future upload({required OtlpPackage package}) async { + if (throwError) { + throw Exception('Upload error'); + } + return resultToReturn; + } +} + +class MockPathProviderPlatform extends PathProviderPlatform { + @override + Future getApplicationSupportPath() async { + final directory = await Directory.systemTemp.createTemp('bugsnag_test'); + return directory.path; + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('FileRetryQueue', () { + late FileRetryQueue retryQueue; + late TestUploader testUploader; + late Directory mockCacheDirectory; + + setUp(() async { + BugsnagClockImpl.ensureInitialized(); + PathProviderPlatform.instance = MockPathProviderPlatform(); + final supportDir = await getApplicationSupportDirectory(); + final mockCachePath = '${supportDir.path}/bugsnag-performance/v1/batches'; + mockCacheDirectory = Directory(mockCachePath); + await mockCacheDirectory.create(recursive: true); + + }); + + tearDown(() async { + if (await mockCacheDirectory.exists()) { + await mockCacheDirectory.delete(recursive: true); + } + }); + + test('should delete the file after successful upload', () async { + // Setup uploader to return success + testUploader = TestUploader(resultToReturn: RequestResult.success); + retryQueue = FileRetryQueue(testUploader, cacheDirectory: mockCacheDirectory); + + // Create a valid payload file + final fileName = '${mockCacheDirectory.path}/payload_success.json'; + final file = File(fileName); + final payloadModel = CachedPayloadModel(headers: {}, body: Uint8List(0)); + await file.writeAsString(jsonEncode(payloadModel.toJson())); + + await retryQueue.flush(); + + // Ensure the file is deleted after flush + expect(await file.exists(), isFalse); + }); + + test('should not delete the file after failed upload', () async { + // Setup uploader to return failure + testUploader = TestUploader(resultToReturn: RequestResult.retriableFailure); + retryQueue = FileRetryQueue(testUploader, cacheDirectory: mockCacheDirectory); + + // Create a valid payload file + final fileName = '${mockCacheDirectory.path}/payload_failure.json'; + final file = File(fileName); + final payloadModel = CachedPayloadModel(headers: {}, body: Uint8List(0)); + await file.writeAsString(jsonEncode(payloadModel.toJson())); + + await retryQueue.flush(); + + // Ensure the file is not deleted after flush + expect(await file.exists(), isTrue); + }); + + test('should handle malformed JSON and delete the file', () async { + testUploader = TestUploader(); + retryQueue = FileRetryQueue(testUploader, cacheDirectory: mockCacheDirectory); + // Create a file with malformed JSON + final fileName = '${mockCacheDirectory.path}/malformed_payload.json'; + final file = File(fileName); + await file.writeAsString('malformed_json'); + + await retryQueue.flush(); + + // Ensure the file is deleted after flush + expect(await file.exists(), isFalse); + }); + + test('should delete files older than 24 hours', () async { + testUploader = TestUploader(); + retryQueue = FileRetryQueue(testUploader, cacheDirectory: mockCacheDirectory); + // Create a file older than 24 hours + final oldTimestamp = BugsnagClockImpl.instance.now().subtract(const Duration(hours: 25)); + final fileName = '${mockCacheDirectory.path}/old_payload.json'; + final file = File(fileName); + await file.writeAsString('{"headers": {}, "body": ""}'); + file.setLastModifiedSync(oldTimestamp); + + await retryQueue.flush(); + + // Ensure the file is deleted after flush + expect(file.existsSync(), isFalse); + }); + }); +} diff --git a/packages/bugsnag_flutter_performance/test/src/uploader/span_batch_test.dart b/packages/bugsnag_flutter_performance/test/src/uploader/span_batch_test.dart index b657621..684014f 100644 --- a/packages/bugsnag_flutter_performance/test/src/uploader/span_batch_test.dart +++ b/packages/bugsnag_flutter_performance/test/src/uploader/span_batch_test.dart @@ -1,5 +1,6 @@ import 'package:bugsnag_flutter_performance/src/configuration.dart'; import 'package:bugsnag_flutter_performance/src/span.dart'; +import 'package:bugsnag_flutter_performance/src/span_attributes_limits.dart'; import 'package:bugsnag_flutter_performance/src/uploader/span_batch.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -32,7 +33,14 @@ void main() { setUp(() { batch.configure( - BugsnagPerformanceConfiguration()..maxBatchSize = batchSize, + BugsnagPerformanceConfiguration( + attributeCountLimit: SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.attributeCountLimit), + attributeStringValueLimit: SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.stringValueLimit), + attributeArrayLengthLimit: SpanAttributesLimits.limitValue( + type: SpanAttributesLimitType.arrayLengthLimit), + )..maxBatchSize = batchSize, ); });