From d98c7f2d781bb0b9b8b19e8827ee65586d8fba83 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Tue, 21 Mar 2023 17:41:07 +0100 Subject: [PATCH 1/9] Add craft to sqflite (#1349) --- .craft.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.craft.yml b/.craft.yml index 9c91cb1166..94d538b336 100644 --- a/.craft.yml +++ b/.craft.yml @@ -19,4 +19,4 @@ targets: pub:sentry_logging: pub:sentry_dio: pub:sentry_file: - #pub:sentry_sqflite: + pub:sentry_sqflite: From 519423f251b6c787fd39742528f413087966d476 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 09:07:01 +0100 Subject: [PATCH 2/9] chore(deps): update Cocoa SDK to v8.3.2 (#1350) Co-authored-by: GitHub --- CHANGELOG.md | 8 ++++++++ flutter/ios/sentry_flutter.podspec | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 715ef9d991..3ae4aa0928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Dependencies + +- Bump Cocoa SDK from v8.3.1 to v8.3.2 ([#1350](https://github.com/getsentry/sentry-dart/pull/1350)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#832) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.3.1...8.3.2) + ## 7.2.0 ### Features diff --git a/flutter/ios/sentry_flutter.podspec b/flutter/ios/sentry_flutter.podspec index d00923f9ff..8ea271f038 100644 --- a/flutter/ios/sentry_flutter.podspec +++ b/flutter/ios/sentry_flutter.podspec @@ -12,7 +12,7 @@ Sentry SDK for Flutter with support to native through sentry-cocoa. :tag => s.version.to_s } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' - s.dependency 'Sentry/HybridSDK', '8.3.1' + s.dependency 'Sentry/HybridSDK', '8.3.2' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' s.ios.deployment_target = '11.0' From 89ea268a7f626fd1b23c6ffa55ec68da030ea450 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Wed, 22 Mar 2023 12:02:22 +0100 Subject: [PATCH 3/9] Open source attribution to adapted repos (#1351) --- file/lib/src/sentry_file.dart | 201 +++++++++++++++++ flutter/lib/src/jvm/jvm_exception.dart | 203 +++++++++++++++++ .../sentry_user_interaction_widget.dart | 205 +++++++++++++++++- .../view_hierarchy/sentry_tree_walker.dart | 205 +++++++++++++++++- 4 files changed, 810 insertions(+), 4 deletions(-) diff --git a/file/lib/src/sentry_file.dart b/file/lib/src/sentry_file.dart index c357990538..2cb667e6ea 100644 --- a/file/lib/src/sentry_file.dart +++ b/file/lib/src/sentry_file.dart @@ -1,4 +1,205 @@ // Adapted from https://github.com/ueman/sentry-dart-tools/blob/8e41418c0f2c62dc88292cf32a4f22e79112b744/sentry_plus/lib/src/file/sentry_file.dart +// Apache License +// Version 2.0, January 2004 +// http://www.apache.org/licenses/ + +// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +// 1. Definitions. + +// "License" shall mean the terms and conditions for use, reproduction, +// and distribution as defined by Sections 1 through 9 of this document. + +// "Licensor" shall mean the copyright owner or entity authorized by +// the copyright owner that is granting the License. + +// "Legal Entity" shall mean the union of the acting entity and all +// other entities that control, are controlled by, or are under common +// control with that entity. For the purposes of this definition, +// "control" means (i) the power, direct or indirect, to cause the +// direction or management of such entity, whether by contract or +// otherwise, or (ii) ownership of fifty percent (50%) or more of the +// outstanding shares, or (iii) beneficial ownership of such entity. + +// "You" (or "Your") shall mean an individual or Legal Entity +// exercising permissions granted by this License. + +// "Source" form shall mean the preferred form for making modifications, +// including but not limited to software source code, documentation +// source, and configuration files. + +// "Object" form shall mean any form resulting from mechanical +// transformation or translation of a Source form, including but +// not limited to compiled object code, generated documentation, +// and conversions to other media types. + +// "Work" shall mean the work of authorship, whether in Source or +// Object form, made available under the License, as indicated by a +// copyright notice that is included in or attached to the work +// (an example is provided in the Appendix below). + +// "Derivative Works" shall mean any work, whether in Source or Object +// form, that is based on (or derived from) the Work and for which the +// editorial revisions, annotations, elaborations, or other modifications +// represent, as a whole, an original work of authorship. For the purposes +// of this License, Derivative Works shall not include works that remain +// separable from, or merely link (or bind by name) to the interfaces of, +// the Work and Derivative Works thereof. + +// "Contribution" shall mean any work of authorship, including +// the original version of the Work and any modifications or additions +// to that Work or Derivative Works thereof, that is intentionally +// submitted to Licensor for inclusion in the Work by the copyright owner +// or by an individual or Legal Entity authorized to submit on behalf of +// the copyright owner. For the purposes of this definition, "submitted" +// means any form of electronic, verbal, or written communication sent +// to the Licensor or its representatives, including but not limited to +// communication on electronic mailing lists, source code control systems, +// and issue tracking systems that are managed by, or on behalf of, the +// Licensor for the purpose of discussing and improving the Work, but +// excluding communication that is conspicuously marked or otherwise +// designated in writing by the copyright owner as "Not a Contribution." + +// "Contributor" shall mean Licensor and any individual or Legal Entity +// on behalf of whom a Contribution has been received by Licensor and +// subsequently incorporated within the Work. + +// 2. Grant of Copyright License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// copyright license to reproduce, prepare Derivative Works of, +// publicly display, publicly perform, sublicense, and distribute the +// Work and such Derivative Works in Source or Object form. + +// 3. Grant of Patent License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// (except as stated in this section) patent license to make, have made, +// use, offer to sell, sell, import, and otherwise transfer the Work, +// where such license applies only to those patent claims licensable +// by such Contributor that are necessarily infringed by their +// Contribution(s) alone or by combination of their Contribution(s) +// with the Work to which such Contribution(s) was submitted. If You +// institute patent litigation against any entity (including a +// cross-claim or counterclaim in a lawsuit) alleging that the Work +// or a Contribution incorporated within the Work constitutes direct +// or contributory patent infringement, then any patent licenses +// granted to You under this License for that Work shall terminate +// as of the date such litigation is filed. + +// 4. Redistribution. You may reproduce and distribute copies of the +// Work or Derivative Works thereof in any medium, with or without +// modifications, and in Source or Object form, provided that You +// meet the following conditions: + +// (a) You must give any other recipients of the Work or +// Derivative Works a copy of this License; and + +// (b) You must cause any modified files to carry prominent notices +// stating that You changed the files; and + +// (c) You must retain, in the Source form of any Derivative Works +// that You distribute, all copyright, patent, trademark, and +// attribution notices from the Source form of the Work, +// excluding those notices that do not pertain to any part of +// the Derivative Works; and + +// (d) If the Work includes a "NOTICE" text file as part of its +// distribution, then any Derivative Works that You distribute must +// include a readable copy of the attribution notices contained +// within such NOTICE file, excluding those notices that do not +// pertain to any part of the Derivative Works, in at least one +// of the following places: within a NOTICE text file distributed +// as part of the Derivative Works; within the Source form or +// documentation, if provided along with the Derivative Works; or, +// within a display generated by the Derivative Works, if and +// wherever such third-party notices normally appear. The contents +// of the NOTICE file are for informational purposes only and +// do not modify the License. You may add Your own attribution +// notices within Derivative Works that You distribute, alongside +// or as an addendum to the NOTICE text from the Work, provided +// that such additional attribution notices cannot be construed +// as modifying the License. + +// You may add Your own copyright statement to Your modifications and +// may provide additional or different license terms and conditions +// for use, reproduction, or distribution of Your modifications, or +// for any such Derivative Works as a whole, provided Your use, +// reproduction, and distribution of the Work otherwise complies with +// the conditions stated in this License. + +// 5. Submission of Contributions. Unless You explicitly state otherwise, +// any Contribution intentionally submitted for inclusion in the Work +// by You to the Licensor shall be under the terms and conditions of +// this License, without any additional terms or conditions. +// Notwithstanding the above, nothing herein shall supersede or modify +// the terms of any separate license agreement you may have executed +// with Licensor regarding such Contributions. + +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor, +// except as required for reasonable and customary use in describing the +// origin of the Work and reproducing the content of the NOTICE file. + +// 7. Disclaimer of Warranty. Unless required by applicable law or +// agreed to in writing, Licensor provides the Work (and each +// Contributor provides its Contributions) on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied, including, without limitation, any warranties or conditions +// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +// PARTICULAR PURPOSE. You are solely responsible for determining the +// appropriateness of using or redistributing the Work and assume any +// risks associated with Your exercise of permissions under this License. + +// 8. Limitation of Liability. In no event and under no legal theory, +// whether in tort (including negligence), contract, or otherwise, +// unless required by applicable law (such as deliberate and grossly +// negligent acts) or agreed to in writing, shall any Contributor be +// liable to You for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a +// result of this License or out of the use or inability to use the +// Work (including but not limited to damages for loss of goodwill, +// work stoppage, computer failure or malfunction, or any and all +// other commercial damages or losses), even if such Contributor +// has been advised of the possibility of such damages. + +// 9. Accepting Warranty or Additional Liability. While redistributing +// the Work or Derivative Works thereof, You may choose to offer, +// and charge a fee for, acceptance of support, warranty, indemnity, +// or other liability obligations and/or rights consistent with this +// License. However, in accepting such obligations, You may act only +// on Your own behalf and on Your sole responsibility, not on behalf +// of any other Contributor, and only if You agree to indemnify, +// defend, and hold each Contributor harmless for any liability +// incurred by, or claims asserted against, such Contributor by reason +// of your accepting any such warranty or additional liability. + +// END OF TERMS AND CONDITIONS + +// APPENDIX: How to apply the Apache License to your work. + +// To apply the Apache License to your work, attach the following +// boilerplate notice, with the fields enclosed by brackets "[]" +// replaced with your own identifying information. (Don't include +// the brackets!) The text should be enclosed in the appropriate +// comment syntax for the file format. We also recommend that a +// file or class name and description of purpose be included on the +// same "printed page" as the copyright notice for easier +// identification within third-party archives. + +// Copyright 2022 Jonas Uekötter + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // ignore_for_file: invalid_use_of_internal_member diff --git a/flutter/lib/src/jvm/jvm_exception.dart b/flutter/lib/src/jvm/jvm_exception.dart index 5bb086c09a..adaf71cac1 100644 --- a/flutter/lib/src/jvm/jvm_exception.dart +++ b/flutter/lib/src/jvm/jvm_exception.dart @@ -1,3 +1,206 @@ +// Adapted from https://github.com/ueman/stack_trace_parser +// Apache License +// Version 2.0, January 2004 +// http://www.apache.org/licenses/ + +// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +// 1. Definitions. + +// "License" shall mean the terms and conditions for use, reproduction, +// and distribution as defined by Sections 1 through 9 of this document. + +// "Licensor" shall mean the copyright owner or entity authorized by +// the copyright owner that is granting the License. + +// "Legal Entity" shall mean the union of the acting entity and all +// other entities that control, are controlled by, or are under common +// control with that entity. For the purposes of this definition, +// "control" means (i) the power, direct or indirect, to cause the +// direction or management of such entity, whether by contract or +// otherwise, or (ii) ownership of fifty percent (50%) or more of the +// outstanding shares, or (iii) beneficial ownership of such entity. + +// "You" (or "Your") shall mean an individual or Legal Entity +// exercising permissions granted by this License. + +// "Source" form shall mean the preferred form for making modifications, +// including but not limited to software source code, documentation +// source, and configuration files. + +// "Object" form shall mean any form resulting from mechanical +// transformation or translation of a Source form, including but +// not limited to compiled object code, generated documentation, +// and conversions to other media types. + +// "Work" shall mean the work of authorship, whether in Source or +// Object form, made available under the License, as indicated by a +// copyright notice that is included in or attached to the work +// (an example is provided in the Appendix below). + +// "Derivative Works" shall mean any work, whether in Source or Object +// form, that is based on (or derived from) the Work and for which the +// editorial revisions, annotations, elaborations, or other modifications +// represent, as a whole, an original work of authorship. For the purposes +// of this License, Derivative Works shall not include works that remain +// separable from, or merely link (or bind by name) to the interfaces of, +// the Work and Derivative Works thereof. + +// "Contribution" shall mean any work of authorship, including +// the original version of the Work and any modifications or additions +// to that Work or Derivative Works thereof, that is intentionally +// submitted to Licensor for inclusion in the Work by the copyright owner +// or by an individual or Legal Entity authorized to submit on behalf of +// the copyright owner. For the purposes of this definition, "submitted" +// means any form of electronic, verbal, or written communication sent +// to the Licensor or its representatives, including but not limited to +// communication on electronic mailing lists, source code control systems, +// and issue tracking systems that are managed by, or on behalf of, the +// Licensor for the purpose of discussing and improving the Work, but +// excluding communication that is conspicuously marked or otherwise +// designated in writing by the copyright owner as "Not a Contribution." + +// "Contributor" shall mean Licensor and any individual or Legal Entity +// on behalf of whom a Contribution has been received by Licensor and +// subsequently incorporated within the Work. + +// 2. Grant of Copyright License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// copyright license to reproduce, prepare Derivative Works of, +// publicly display, publicly perform, sublicense, and distribute the +// Work and such Derivative Works in Source or Object form. + +// 3. Grant of Patent License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// (except as stated in this section) patent license to make, have made, +// use, offer to sell, sell, import, and otherwise transfer the Work, +// where such license applies only to those patent claims licensable +// by such Contributor that are necessarily infringed by their +// Contribution(s) alone or by combination of their Contribution(s) +// with the Work to which such Contribution(s) was submitted. If You +// institute patent litigation against any entity (including a +// cross-claim or counterclaim in a lawsuit) alleging that the Work +// or a Contribution incorporated within the Work constitutes direct +// or contributory patent infringement, then any patent licenses +// granted to You under this License for that Work shall terminate +// as of the date such litigation is filed. + +// 4. Redistribution. You may reproduce and distribute copies of the +// Work or Derivative Works thereof in any medium, with or without +// modifications, and in Source or Object form, provided that You +// meet the following conditions: + +// (a) You must give any other recipients of the Work or +// Derivative Works a copy of this License; and + +// (b) You must cause any modified files to carry prominent notices +// stating that You changed the files; and + +// (c) You must retain, in the Source form of any Derivative Works +// that You distribute, all copyright, patent, trademark, and +// attribution notices from the Source form of the Work, +// excluding those notices that do not pertain to any part of +// the Derivative Works; and + +// (d) If the Work includes a "NOTICE" text file as part of its +// distribution, then any Derivative Works that You distribute must +// include a readable copy of the attribution notices contained +// within such NOTICE file, excluding those notices that do not +// pertain to any part of the Derivative Works, in at least one +// of the following places: within a NOTICE text file distributed +// as part of the Derivative Works; within the Source form or +// documentation, if provided along with the Derivative Works; or, +// within a display generated by the Derivative Works, if and +// wherever such third-party notices normally appear. The contents +// of the NOTICE file are for informational purposes only and +// do not modify the License. You may add Your own attribution +// notices within Derivative Works that You distribute, alongside +// or as an addendum to the NOTICE text from the Work, provided +// that such additional attribution notices cannot be construed +// as modifying the License. + +// You may add Your own copyright statement to Your modifications and +// may provide additional or different license terms and conditions +// for use, reproduction, or distribution of Your modifications, or +// for any such Derivative Works as a whole, provided Your use, +// reproduction, and distribution of the Work otherwise complies with +// the conditions stated in this License. + +// 5. Submission of Contributions. Unless You explicitly state otherwise, +// any Contribution intentionally submitted for inclusion in the Work +// by You to the Licensor shall be under the terms and conditions of +// this License, without any additional terms or conditions. +// Notwithstanding the above, nothing herein shall supersede or modify +// the terms of any separate license agreement you may have executed +// with Licensor regarding such Contributions. + +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor, +// except as required for reasonable and customary use in describing the +// origin of the Work and reproducing the content of the NOTICE file. + +// 7. Disclaimer of Warranty. Unless required by applicable law or +// agreed to in writing, Licensor provides the Work (and each +// Contributor provides its Contributions) on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied, including, without limitation, any warranties or conditions +// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +// PARTICULAR PURPOSE. You are solely responsible for determining the +// appropriateness of using or redistributing the Work and assume any +// risks associated with Your exercise of permissions under this License. + +// 8. Limitation of Liability. In no event and under no legal theory, +// whether in tort (including negligence), contract, or otherwise, +// unless required by applicable law (such as deliberate and grossly +// negligent acts) or agreed to in writing, shall any Contributor be +// liable to You for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a +// result of this License or out of the use or inability to use the +// Work (including but not limited to damages for loss of goodwill, +// work stoppage, computer failure or malfunction, or any and all +// other commercial damages or losses), even if such Contributor +// has been advised of the possibility of such damages. + +// 9. Accepting Warranty or Additional Liability. While redistributing +// the Work or Derivative Works thereof, You may choose to offer, +// and charge a fee for, acceptance of support, warranty, indemnity, +// or other liability obligations and/or rights consistent with this +// License. However, in accepting such obligations, You may act only +// on Your own behalf and on Your sole responsibility, not on behalf +// of any other Contributor, and only if You agree to indemnify, +// defend, and hold each Contributor harmless for any liability +// incurred by, or claims asserted against, such Contributor by reason +// of your accepting any such warranty or additional liability. + +// END OF TERMS AND CONDITIONS + +// APPENDIX: How to apply the Apache License to your work. + +// To apply the Apache License to your work, attach the following +// boilerplate notice, with the fields enclosed by brackets "[]" +// replaced with your own identifying information. (Don't include +// the brackets!) The text should be enclosed in the appropriate +// comment syntax for the file format. We also recommend that a +// file or class name and description of purpose be included on the +// same "printed page" as the copyright notice for easier +// identification within third-party archives. + +// Copyright 2022 Jonas Uekötter + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import 'jvm_frame.dart'; const _emptyString = ''; diff --git a/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart b/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart index 0f4eb1951c..2e17b39578 100644 --- a/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart +++ b/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart @@ -1,3 +1,206 @@ +// Adapted from https://github.com/ueman/sentry-dart-tools/blob/8e41418c0f2c62dc88292cf32a4f22e79112b744/sentry_flutter_plus/lib/src/widgets/click_tracker.dart +// Apache License +// Version 2.0, January 2004 +// http://www.apache.org/licenses/ + +// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +// 1. Definitions. + +// "License" shall mean the terms and conditions for use, reproduction, +// and distribution as defined by Sections 1 through 9 of this document. + +// "Licensor" shall mean the copyright owner or entity authorized by +// the copyright owner that is granting the License. + +// "Legal Entity" shall mean the union of the acting entity and all +// other entities that control, are controlled by, or are under common +// control with that entity. For the purposes of this definition, +// "control" means (i) the power, direct or indirect, to cause the +// direction or management of such entity, whether by contract or +// otherwise, or (ii) ownership of fifty percent (50%) or more of the +// outstanding shares, or (iii) beneficial ownership of such entity. + +// "You" (or "Your") shall mean an individual or Legal Entity +// exercising permissions granted by this License. + +// "Source" form shall mean the preferred form for making modifications, +// including but not limited to software source code, documentation +// source, and configuration files. + +// "Object" form shall mean any form resulting from mechanical +// transformation or translation of a Source form, including but +// not limited to compiled object code, generated documentation, +// and conversions to other media types. + +// "Work" shall mean the work of authorship, whether in Source or +// Object form, made available under the License, as indicated by a +// copyright notice that is included in or attached to the work +// (an example is provided in the Appendix below). + +// "Derivative Works" shall mean any work, whether in Source or Object +// form, that is based on (or derived from) the Work and for which the +// editorial revisions, annotations, elaborations, or other modifications +// represent, as a whole, an original work of authorship. For the purposes +// of this License, Derivative Works shall not include works that remain +// separable from, or merely link (or bind by name) to the interfaces of, +// the Work and Derivative Works thereof. + +// "Contribution" shall mean any work of authorship, including +// the original version of the Work and any modifications or additions +// to that Work or Derivative Works thereof, that is intentionally +// submitted to Licensor for inclusion in the Work by the copyright owner +// or by an individual or Legal Entity authorized to submit on behalf of +// the copyright owner. For the purposes of this definition, "submitted" +// means any form of electronic, verbal, or written communication sent +// to the Licensor or its representatives, including but not limited to +// communication on electronic mailing lists, source code control systems, +// and issue tracking systems that are managed by, or on behalf of, the +// Licensor for the purpose of discussing and improving the Work, but +// excluding communication that is conspicuously marked or otherwise +// designated in writing by the copyright owner as "Not a Contribution." + +// "Contributor" shall mean Licensor and any individual or Legal Entity +// on behalf of whom a Contribution has been received by Licensor and +// subsequently incorporated within the Work. + +// 2. Grant of Copyright License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// copyright license to reproduce, prepare Derivative Works of, +// publicly display, publicly perform, sublicense, and distribute the +// Work and such Derivative Works in Source or Object form. + +// 3. Grant of Patent License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// (except as stated in this section) patent license to make, have made, +// use, offer to sell, sell, import, and otherwise transfer the Work, +// where such license applies only to those patent claims licensable +// by such Contributor that are necessarily infringed by their +// Contribution(s) alone or by combination of their Contribution(s) +// with the Work to which such Contribution(s) was submitted. If You +// institute patent litigation against any entity (including a +// cross-claim or counterclaim in a lawsuit) alleging that the Work +// or a Contribution incorporated within the Work constitutes direct +// or contributory patent infringement, then any patent licenses +// granted to You under this License for that Work shall terminate +// as of the date such litigation is filed. + +// 4. Redistribution. You may reproduce and distribute copies of the +// Work or Derivative Works thereof in any medium, with or without +// modifications, and in Source or Object form, provided that You +// meet the following conditions: + +// (a) You must give any other recipients of the Work or +// Derivative Works a copy of this License; and + +// (b) You must cause any modified files to carry prominent notices +// stating that You changed the files; and + +// (c) You must retain, in the Source form of any Derivative Works +// that You distribute, all copyright, patent, trademark, and +// attribution notices from the Source form of the Work, +// excluding those notices that do not pertain to any part of +// the Derivative Works; and + +// (d) If the Work includes a "NOTICE" text file as part of its +// distribution, then any Derivative Works that You distribute must +// include a readable copy of the attribution notices contained +// within such NOTICE file, excluding those notices that do not +// pertain to any part of the Derivative Works, in at least one +// of the following places: within a NOTICE text file distributed +// as part of the Derivative Works; within the Source form or +// documentation, if provided along with the Derivative Works; or, +// within a display generated by the Derivative Works, if and +// wherever such third-party notices normally appear. The contents +// of the NOTICE file are for informational purposes only and +// do not modify the License. You may add Your own attribution +// notices within Derivative Works that You distribute, alongside +// or as an addendum to the NOTICE text from the Work, provided +// that such additional attribution notices cannot be construed +// as modifying the License. + +// You may add Your own copyright statement to Your modifications and +// may provide additional or different license terms and conditions +// for use, reproduction, or distribution of Your modifications, or +// for any such Derivative Works as a whole, provided Your use, +// reproduction, and distribution of the Work otherwise complies with +// the conditions stated in this License. + +// 5. Submission of Contributions. Unless You explicitly state otherwise, +// any Contribution intentionally submitted for inclusion in the Work +// by You to the Licensor shall be under the terms and conditions of +// this License, without any additional terms or conditions. +// Notwithstanding the above, nothing herein shall supersede or modify +// the terms of any separate license agreement you may have executed +// with Licensor regarding such Contributions. + +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor, +// except as required for reasonable and customary use in describing the +// origin of the Work and reproducing the content of the NOTICE file. + +// 7. Disclaimer of Warranty. Unless required by applicable law or +// agreed to in writing, Licensor provides the Work (and each +// Contributor provides its Contributions) on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied, including, without limitation, any warranties or conditions +// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +// PARTICULAR PURPOSE. You are solely responsible for determining the +// appropriateness of using or redistributing the Work and assume any +// risks associated with Your exercise of permissions under this License. + +// 8. Limitation of Liability. In no event and under no legal theory, +// whether in tort (including negligence), contract, or otherwise, +// unless required by applicable law (such as deliberate and grossly +// negligent acts) or agreed to in writing, shall any Contributor be +// liable to You for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a +// result of this License or out of the use or inability to use the +// Work (including but not limited to damages for loss of goodwill, +// work stoppage, computer failure or malfunction, or any and all +// other commercial damages or losses), even if such Contributor +// has been advised of the possibility of such damages. + +// 9. Accepting Warranty or Additional Liability. While redistributing +// the Work or Derivative Works thereof, You may choose to offer, +// and charge a fee for, acceptance of support, warranty, indemnity, +// or other liability obligations and/or rights consistent with this +// License. However, in accepting such obligations, You may act only +// on Your own behalf and on Your sole responsibility, not on behalf +// of any other Contributor, and only if You agree to indemnify, +// defend, and hold each Contributor harmless for any liability +// incurred by, or claims asserted against, such Contributor by reason +// of your accepting any such warranty or additional liability. + +// END OF TERMS AND CONDITIONS + +// APPENDIX: How to apply the Apache License to your work. + +// To apply the Apache License to your work, attach the following +// boilerplate notice, with the fields enclosed by brackets "[]" +// replaced with your own identifying information. (Don't include +// the brackets!) The text should be enclosed in the appropriate +// comment syntax for the file format. We also recommend that a +// file or class name and description of purpose be included on the +// same "printed page" as the copyright notice for easier +// identification within third-party archives. + +// Copyright 2022 Jonas Uekötter + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -7,8 +210,6 @@ import '../../sentry_flutter.dart'; import '../widget_utils.dart'; import 'user_interaction_widget.dart'; -// Adapted from https://github.com/ueman/sentry-dart-tools/blob/8e41418c0f2c62dc88292cf32a4f22e79112b744/sentry_flutter_plus/lib/src/widgets/click_tracker.dart - const _tapDeltaArea = 20 * 20; Element? _clickTrackerElement; diff --git a/flutter/lib/src/view_hierarchy/sentry_tree_walker.dart b/flutter/lib/src/view_hierarchy/sentry_tree_walker.dart index 2e0f1aa594..aaa21143ed 100644 --- a/flutter/lib/src/view_hierarchy/sentry_tree_walker.dart +++ b/flutter/lib/src/view_hierarchy/sentry_tree_walker.dart @@ -1,10 +1,211 @@ +// adapted from https://github.com/ueman/sentry-dart-tools/blob/8e41418c0f2c62dc88292cf32a4f22e79112b744/sentry_flutter_plus/lib/src/integrations/tree_walker_integration.dart +// Apache License +// Version 2.0, January 2004 +// http://www.apache.org/licenses/ + +// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +// 1. Definitions. + +// "License" shall mean the terms and conditions for use, reproduction, +// and distribution as defined by Sections 1 through 9 of this document. + +// "Licensor" shall mean the copyright owner or entity authorized by +// the copyright owner that is granting the License. + +// "Legal Entity" shall mean the union of the acting entity and all +// other entities that control, are controlled by, or are under common +// control with that entity. For the purposes of this definition, +// "control" means (i) the power, direct or indirect, to cause the +// direction or management of such entity, whether by contract or +// otherwise, or (ii) ownership of fifty percent (50%) or more of the +// outstanding shares, or (iii) beneficial ownership of such entity. + +// "You" (or "Your") shall mean an individual or Legal Entity +// exercising permissions granted by this License. + +// "Source" form shall mean the preferred form for making modifications, +// including but not limited to software source code, documentation +// source, and configuration files. + +// "Object" form shall mean any form resulting from mechanical +// transformation or translation of a Source form, including but +// not limited to compiled object code, generated documentation, +// and conversions to other media types. + +// "Work" shall mean the work of authorship, whether in Source or +// Object form, made available under the License, as indicated by a +// copyright notice that is included in or attached to the work +// (an example is provided in the Appendix below). + +// "Derivative Works" shall mean any work, whether in Source or Object +// form, that is based on (or derived from) the Work and for which the +// editorial revisions, annotations, elaborations, or other modifications +// represent, as a whole, an original work of authorship. For the purposes +// of this License, Derivative Works shall not include works that remain +// separable from, or merely link (or bind by name) to the interfaces of, +// the Work and Derivative Works thereof. + +// "Contribution" shall mean any work of authorship, including +// the original version of the Work and any modifications or additions +// to that Work or Derivative Works thereof, that is intentionally +// submitted to Licensor for inclusion in the Work by the copyright owner +// or by an individual or Legal Entity authorized to submit on behalf of +// the copyright owner. For the purposes of this definition, "submitted" +// means any form of electronic, verbal, or written communication sent +// to the Licensor or its representatives, including but not limited to +// communication on electronic mailing lists, source code control systems, +// and issue tracking systems that are managed by, or on behalf of, the +// Licensor for the purpose of discussing and improving the Work, but +// excluding communication that is conspicuously marked or otherwise +// designated in writing by the copyright owner as "Not a Contribution." + +// "Contributor" shall mean Licensor and any individual or Legal Entity +// on behalf of whom a Contribution has been received by Licensor and +// subsequently incorporated within the Work. + +// 2. Grant of Copyright License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// copyright license to reproduce, prepare Derivative Works of, +// publicly display, publicly perform, sublicense, and distribute the +// Work and such Derivative Works in Source or Object form. + +// 3. Grant of Patent License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// (except as stated in this section) patent license to make, have made, +// use, offer to sell, sell, import, and otherwise transfer the Work, +// where such license applies only to those patent claims licensable +// by such Contributor that are necessarily infringed by their +// Contribution(s) alone or by combination of their Contribution(s) +// with the Work to which such Contribution(s) was submitted. If You +// institute patent litigation against any entity (including a +// cross-claim or counterclaim in a lawsuit) alleging that the Work +// or a Contribution incorporated within the Work constitutes direct +// or contributory patent infringement, then any patent licenses +// granted to You under this License for that Work shall terminate +// as of the date such litigation is filed. + +// 4. Redistribution. You may reproduce and distribute copies of the +// Work or Derivative Works thereof in any medium, with or without +// modifications, and in Source or Object form, provided that You +// meet the following conditions: + +// (a) You must give any other recipients of the Work or +// Derivative Works a copy of this License; and + +// (b) You must cause any modified files to carry prominent notices +// stating that You changed the files; and + +// (c) You must retain, in the Source form of any Derivative Works +// that You distribute, all copyright, patent, trademark, and +// attribution notices from the Source form of the Work, +// excluding those notices that do not pertain to any part of +// the Derivative Works; and + +// (d) If the Work includes a "NOTICE" text file as part of its +// distribution, then any Derivative Works that You distribute must +// include a readable copy of the attribution notices contained +// within such NOTICE file, excluding those notices that do not +// pertain to any part of the Derivative Works, in at least one +// of the following places: within a NOTICE text file distributed +// as part of the Derivative Works; within the Source form or +// documentation, if provided along with the Derivative Works; or, +// within a display generated by the Derivative Works, if and +// wherever such third-party notices normally appear. The contents +// of the NOTICE file are for informational purposes only and +// do not modify the License. You may add Your own attribution +// notices within Derivative Works that You distribute, alongside +// or as an addendum to the NOTICE text from the Work, provided +// that such additional attribution notices cannot be construed +// as modifying the License. + +// You may add Your own copyright statement to Your modifications and +// may provide additional or different license terms and conditions +// for use, reproduction, or distribution of Your modifications, or +// for any such Derivative Works as a whole, provided Your use, +// reproduction, and distribution of the Work otherwise complies with +// the conditions stated in this License. + +// 5. Submission of Contributions. Unless You explicitly state otherwise, +// any Contribution intentionally submitted for inclusion in the Work +// by You to the Licensor shall be under the terms and conditions of +// this License, without any additional terms or conditions. +// Notwithstanding the above, nothing herein shall supersede or modify +// the terms of any separate license agreement you may have executed +// with Licensor regarding such Contributions. + +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor, +// except as required for reasonable and customary use in describing the +// origin of the Work and reproducing the content of the NOTICE file. + +// 7. Disclaimer of Warranty. Unless required by applicable law or +// agreed to in writing, Licensor provides the Work (and each +// Contributor provides its Contributions) on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied, including, without limitation, any warranties or conditions +// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +// PARTICULAR PURPOSE. You are solely responsible for determining the +// appropriateness of using or redistributing the Work and assume any +// risks associated with Your exercise of permissions under this License. + +// 8. Limitation of Liability. In no event and under no legal theory, +// whether in tort (including negligence), contract, or otherwise, +// unless required by applicable law (such as deliberate and grossly +// negligent acts) or agreed to in writing, shall any Contributor be +// liable to You for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a +// result of this License or out of the use or inability to use the +// Work (including but not limited to damages for loss of goodwill, +// work stoppage, computer failure or malfunction, or any and all +// other commercial damages or losses), even if such Contributor +// has been advised of the possibility of such damages. + +// 9. Accepting Warranty or Additional Liability. While redistributing +// the Work or Derivative Works thereof, You may choose to offer, +// and charge a fee for, acceptance of support, warranty, indemnity, +// or other liability obligations and/or rights consistent with this +// License. However, in accepting such obligations, You may act only +// on Your own behalf and on Your sole responsibility, not on behalf +// of any other Contributor, and only if You agree to indemnify, +// defend, and hold each Contributor harmless for any liability +// incurred by, or claims asserted against, such Contributor by reason +// of your accepting any such warranty or additional liability. + +// END OF TERMS AND CONDITIONS + +// APPENDIX: How to apply the Apache License to your work. + +// To apply the Apache License to your work, attach the following +// boilerplate notice, with the fields enclosed by brackets "[]" +// replaced with your own identifying information. (Don't include +// the brackets!) The text should be enclosed in the appropriate +// comment syntax for the file format. We also recommend that a +// file or class name and description of purpose be included on the +// same "printed page" as the copyright notice for easier +// identification within third-party archives. + +// Copyright 2022 Jonas Uekötter + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import 'package:flutter/widgets.dart'; import '../../sentry_flutter.dart'; import '../widget_utils.dart'; -// adapted from https://github.com/ueman/sentry-dart-tools/blob/8e41418c0f2c62dc88292cf32a4f22e79112b744/sentry_flutter_plus/lib/src/integrations/tree_walker_integration.dart - class _TreeWalker { static const _privateDelimiter = '_'; From e2d89fc73b84c81801b638f4be7c488d53b7960d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:37:49 +0200 Subject: [PATCH 4/9] Add missing props (#1354) --- CHANGELOG.md | 4 +++ .../io/sentry/flutter/SentryFlutterPlugin.kt | 2 ++ .../Classes/SentryFlutterPluginApple.swift | 26 ++++++++++++------- .../integrations/native_sdk_integration.dart | 7 +++-- flutter/lib/src/sentry_flutter_options.dart | 5 ++++ .../init_native_sdk_integration_test.dart | 23 ++++++++-------- min_version_test/lib/main.dart | 1 + 7 files changed, 43 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae4aa0928..a221e31ca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#832) - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.3.1...8.3.2) +## Fixes + +- Sync missing properties to the Native SDKs ([#1354](https://github.com/getsentry/sentry-dart/pull/1354)) + ## 7.2.0 ### Features diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 01e5b2f479..ac931193f2 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -166,6 +166,8 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { args.getIfNotNull("sendClientReports") { options.isSendClientReports = it } + args.getIfNotNull("maxAttachmentSize") { options.maxAttachmentSize = it } + val name = "sentry.java.android.flutter" var sdkVersion = options.sdkVersion diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index 7eb9cefdcd..a0d2a802e8 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -313,18 +313,12 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { options.dist = dist } - if let enableAutoNativeBreadcrumbs = arguments["enableAutoNativeBreadcrumbs"] as? Bool, - enableAutoNativeBreadcrumbs == false { - options.integrations = options.integrations?.filter { (name) -> Bool in - name != "SentryAutoBreadcrumbTrackingIntegration" - } + if let enableAutoNativeBreadcrumbs = arguments["enableAutoNativeBreadcrumbs"] as? Bool { + options.enableAutoBreadcrumbTracking = false } - if let enableNativeCrashHandling = arguments["enableNativeCrashHandling"] as? Bool, - enableNativeCrashHandling == false { - options.integrations = options.integrations?.filter { (name) -> Bool in - name != "SentryCrashIntegration" - } + if let enableNativeCrashHandling = arguments["enableNativeCrashHandling"] as? Bool { + options.enableCrashHandler = false } if let maxBreadcrumbs = arguments["maxBreadcrumbs"] as? UInt { @@ -346,6 +340,18 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { if let sendClientReports = arguments["sendClientReports"] as? Bool { options.sendClientReports = sendClientReports } + + if let maxAttachmentSize = arguments["maxAttachmentSize"] as? UInt { + options.maxAttachmentSize = maxAttachmentSize + } + + if let captureFailedRequests = arguments["captureFailedRequests"] as? Bool { + options.enableCaptureFailedRequests = captureFailedRequests + } + + if let enableAppHangTracking = arguments["enableAppHangTracking"] as? Bool { + options.enableAppHangTracking = enableAppHangTracking + } } private func logLevelFrom(diagnosticLevel: String) -> SentryLevel { diff --git a/flutter/lib/src/integrations/native_sdk_integration.dart b/flutter/lib/src/integrations/native_sdk_integration.dart index a8a15a297a..b003391680 100644 --- a/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/flutter/lib/src/integrations/native_sdk_integration.dart @@ -45,11 +45,10 @@ class NativeSdkIntegration extends Integration { 'enableNdkScopeSync': options.enableNdkScopeSync, 'enableAutoPerformanceTracing': options.enableAutoPerformanceTracing, 'sendClientReports': options.sendClientReports, - 'sdk': { - 'name': options.sdk.name, - 'version': options.sdk.version, - }, 'proguardUuid': options.proguardUuid, + 'maxAttachmentSize': options.maxAttachmentSize, + 'captureFailedRequests': options.captureFailedRequests, + 'enableAppHangTracking': options.enableAppHangTracking, }); options.sdk.addIntegration('nativeSdkIntegration'); diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index b61bef2f6e..811bf16781 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -194,6 +194,11 @@ class SentryFlutterOptions extends SentryOptions { @experimental bool attachViewHierarchy = false; + /// When enabled, the SDK tracks when the application stops responding for a + /// specific amount of time (default 2s). + /// Only available on iOS and macOS. + bool enableAppHangTracking = true; + /// By using this, you are disabling native [Breadcrumb] tracking and instead /// you are just tracking [Breadcrumb]s which result from events available /// in the current Flutter environment. diff --git a/flutter/test/integrations/init_native_sdk_integration_test.dart b/flutter/test/integrations/init_native_sdk_integration_test.dart index b3c93738a3..9140e7cc5b 100644 --- a/flutter/test/integrations/init_native_sdk_integration_test.dart +++ b/flutter/test/integrations/init_native_sdk_integration_test.dart @@ -56,11 +56,10 @@ void main() { 'enableNdkScopeSync': true, 'enableAutoPerformanceTracing': true, 'sendClientReports': true, - 'sdk': { - 'name': 'sentry.dart.flutter', - 'version': sdkVersion, - }, - 'proguardUuid': null + 'proguardUuid': null, + 'maxAttachmentSize': 20 * 1024 * 1024, + 'captureFailedRequests': true, + 'enableAppHangTracking': true, }); }); @@ -94,7 +93,10 @@ void main() { ..enableAutoPerformanceTracing = false ..sendClientReports = false ..enableNdkScopeSync = true - ..proguardUuid = fakeProguardUuid; + ..proguardUuid = fakeProguardUuid + ..maxAttachmentSize = 10 + ..captureFailedRequests = false + ..enableAppHangTracking = false; options.sdk.addIntegration('foo'); options.sdk.addPackage('bar', '1'); @@ -131,11 +133,10 @@ void main() { 'enableNdkScopeSync': true, 'enableAutoPerformanceTracing': false, 'sendClientReports': false, - 'sdk': { - 'name': 'sentry.dart.flutter', - 'version': sdkVersion, - }, - 'proguardUuid': fakeProguardUuid + 'proguardUuid': fakeProguardUuid, + 'maxAttachmentSize': 10, + 'captureFailedRequests': false, + 'enableAppHangTracking': false, }); }); diff --git a/min_version_test/lib/main.dart b/min_version_test/lib/main.dart index 3c4067d86f..98c440374e 100644 --- a/min_version_test/lib/main.dart +++ b/min_version_test/lib/main.dart @@ -163,6 +163,7 @@ class _MyHomePageState extends State { ), Text( '$_counter', + // ignore: deprecated_member_use style: Theme.of(context).textTheme.headline4, ), ], From 0d38a287668ffe2bbaf1077c1b64f8a395c63e74 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 27 Mar 2023 11:26:33 +0200 Subject: [PATCH 5/9] Remove dio test with no stack trace, field is not nullable now (#1356) --- dio/test/dio_stacktrace_extractor_test.dart | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dio/test/dio_stacktrace_extractor_test.dart b/dio/test/dio_stacktrace_extractor_test.dart index d83ff1381a..3fffe44278 100644 --- a/dio/test/dio_stacktrace_extractor_test.dart +++ b/dio/test/dio_stacktrace_extractor_test.dart @@ -25,20 +25,6 @@ void main() { expect(result, stacktrace); }); - - test('extracts nothing with missing stacktrace', () { - final sut = fixture.getSut(); - final exception = Exception('foo bar'); - - final dioError = DioError( - error: exception, - requestOptions: RequestOptions(path: '/foo/bar'), - ); - - final result = sut.stackTrace(dioError); - - expect(result, isNull); - }); }); } From 8c65dfef1b11e6081f2c43c9dbbd529e5e74d1c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:46:17 +0000 Subject: [PATCH 6/9] chore(deps): update Cocoa SDK to v8.3.3 (#1355) Co-authored-by: GitHub Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> --- CHANGELOG.md | 6 +++--- flutter/ios/sentry_flutter.podspec | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a221e31ca6..358bf15eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,9 @@ ### Dependencies -- Bump Cocoa SDK from v8.3.1 to v8.3.2 ([#1350](https://github.com/getsentry/sentry-dart/pull/1350)) - - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#832) - - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.3.1...8.3.2) +- Bump Cocoa SDK from v8.3.1 to v8.3.3 ([#1350](https://github.com/getsentry/sentry-dart/pull/1350), [#1355](https://github.com/getsentry/sentry-dart/pull/1355)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#833) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.3.1...8.3.3) ## Fixes diff --git a/flutter/ios/sentry_flutter.podspec b/flutter/ios/sentry_flutter.podspec index 8ea271f038..3524eada70 100644 --- a/flutter/ios/sentry_flutter.podspec +++ b/flutter/ios/sentry_flutter.podspec @@ -12,7 +12,7 @@ Sentry SDK for Flutter with support to native through sentry-cocoa. :tag => s.version.to_s } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' - s.dependency 'Sentry/HybridSDK', '8.3.2' + s.dependency 'Sentry/HybridSDK', '8.3.3' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' s.ios.deployment_target = '11.0' From 8a7f528439e02e7c79819a5988d576778d1f78a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:05:58 +0000 Subject: [PATCH 7/9] chore(deps): update Flutter SDK (metrics) to v3.7.8 (#1352) Co-authored-by: GitHub --- metrics/flutter.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics/flutter.properties b/metrics/flutter.properties index c415acf50f..54cad716ba 100644 --- a/metrics/flutter.properties +++ b/metrics/flutter.properties @@ -1,2 +1,2 @@ -version = 3.7.7 +version = 3.7.8 repo = https://github.com/flutter/flutter From df16b96fceb6ce836e4d52d33120d7b1d922d5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 27 Mar 2023 10:35:04 +0000 Subject: [PATCH 8/9] Vendor `intl` NumberFormat (#1269) Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Co-authored-by: Manoel Aranda Neto --- dart/lib/src/sentry_tracer.dart | 6 +- dart/lib/src/utils/sample_rate_format.dart | 323 +++++++++++++++++++ dart/pubspec.yaml | 2 +- dart/test/utils/sample_rate_format_test.dart | 49 +++ 4 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 dart/lib/src/utils/sample_rate_format.dart create mode 100644 dart/test/utils/sample_rate_format_test.dart diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index ea519eb3bf..622eaaa3da 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:intl/intl.dart'; import 'package:meta/meta.dart'; import '../sentry.dart'; import 'sentry_tracer_finish_status.dart'; +import 'utils/sample_rate_format.dart'; @internal class SentryTracer extends ISentrySpan { @@ -349,9 +349,7 @@ class SentryTracer extends ISentrySpan { if (!isValidSampleRate(sampleRate)) { return null; } - // requires intl package - final formatter = NumberFormat('#.################'); - return formatter.format(sampleRate); + return sampleRate != null ? SampleRateFormat().format(sampleRate) : null; } bool _isHighQualityTransactionName(SentryTransactionNameSource source) { diff --git a/dart/lib/src/utils/sample_rate_format.dart b/dart/lib/src/utils/sample_rate_format.dart new file mode 100644 index 0000000000..4abb79edd8 --- /dev/null +++ b/dart/lib/src/utils/sample_rate_format.dart @@ -0,0 +1,323 @@ +/// Code ported & adapted from `intl` package +/// https://pub.dev/packages/intl +/// +/// License: +/// +/// Copyright 2013, the Dart project authors. +/// +/// Redistribution and use in source and binary forms, with or without +/// modification, are permitted provided that the following conditions are +/// met: +/// +/// * Redistributions of source code must retain the above copyright +/// notice, this list of conditions and the following disclaimer. +/// * Redistributions in binary form must reproduce the above +/// copyright notice, this list of conditions and the following +/// disclaimer in the documentation and/or other materials provided +/// with the distribution. +/// * Neither the name of Google LLC nor the names of its +/// contributors may be used to endorse or promote products derived +/// from this software without specific prior written permission. +/// +/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +/// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +/// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +/// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +/// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +/// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +/// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +/// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +/// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +/// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import 'dart:math'; + +import 'package:meta/meta.dart'; + +@internal +class SampleRateFormat { + int _minimumIntegerDigits; + int _maximumFractionDigits; + int _minimumFractionDigits; + + /// The difference between our zero and '0'. + /// + /// In other words, a constant _localeZero - _zero. Initialized when + /// the locale is set. + final int _zeroOffset; + + /// Caches the symbols + final _NumberSymbols _symbols; + + /// Transient internal state in which to build up the result of the format + /// operation. We can have this be just an instance variable because Dart is + /// single-threaded and unless we do an asynchronous operation in the process + /// of formatting then there will only ever be one number being formatted + /// at a time. In languages with threads we'd need to pass this on the stack. + final StringBuffer _buffer = StringBuffer(); + + factory SampleRateFormat() { + var symbols = _NumberSymbols( + DECIMAL_SEP: '.', + ZERO_DIGIT: '0', + ); + var localeZero = symbols.ZERO_DIGIT.codeUnitAt(0); + var zeroOffset = localeZero - '0'.codeUnitAt(0); + + return SampleRateFormat._( + symbols, + zeroOffset, + ); + } + + SampleRateFormat._(this._symbols, this._zeroOffset) + : _minimumIntegerDigits = 1, + _maximumFractionDigits = 16, + _minimumFractionDigits = 0; + + /// Format the sample rate + String format(dynamic sampleRate) { + try { + if (_isNaN(sampleRate)) return '0'; + if (_isSmallerZero(sampleRate)) { + sampleRate = 0; + } + if (_isLargerOne(sampleRate)) { + sampleRate = 1; + } + _formatFixed(sampleRate.abs()); + + var result = _buffer.toString(); + _buffer.clear(); + return result; + } catch (_) { + _buffer.clear(); + return '0'; + } + } + + /// Used to test if we have exceeded integer limits. + static final _maxInt = 1 is double ? pow(2, 52) : 1.0e300.floor(); + static final _maxDigits = (log(_maxInt) / log(10)).ceil(); + + bool _isNaN(number) => number is num ? number.isNaN : false; + bool _isSmallerZero(number) => number is num ? number < 0 : false; + bool _isLargerOne(number) => number is num ? number > 1 : false; + + /// Format the basic number portion, including the fractional digits. + void _formatFixed(dynamic number) { + dynamic integerPart; + int fractionPart; + int extraIntegerDigits; + var fractionDigits = _maximumFractionDigits; + var minFractionDigits = _minimumFractionDigits; + + var power = 0; + int digitMultiplier; + + // We have three possible pieces. First, the basic integer part. If this + // is a percent or permille, the additional 2 or 3 digits. Finally the + // fractional part. + // We avoid multiplying the number because it might overflow if we have + // a fixed-size integer type, so we extract each of the three as an + // integer pieces. + integerPart = _floor(number); + var fraction = number - integerPart; + if (fraction.toInt() != 0) { + // If the fractional part leftover is > 1, presumbly the number + // was too big for a fixed-size integer, so leave it as whatever + // it was - the obvious thing is a double. + integerPart = number; + fraction = 0; + } + + power = pow(10, fractionDigits) as int; + digitMultiplier = power; + + // Multiply out to the number of decimal places and the percent, then + // round. For fixed-size integer types this should always be zero, so + // multiplying is OK. + var remainingDigits = _round(fraction * digitMultiplier).toInt(); + + if (remainingDigits >= digitMultiplier) { + // Overflow into the main digits: 0.99 => 1.00 + integerPart++; + remainingDigits -= digitMultiplier; + } else if (_numberOfIntegerDigits(remainingDigits) > + _numberOfIntegerDigits(_floor(fraction * digitMultiplier).toInt())) { + // Fraction has been rounded (0.0996 -> 0.1). + fraction = remainingDigits / digitMultiplier; + } + + // Separate out the extra integer parts from the fraction part. + extraIntegerDigits = remainingDigits ~/ power; + fractionPart = remainingDigits % power; + + var integerDigits = _integerDigits(integerPart, extraIntegerDigits); + var digitLength = integerDigits.length; + var fractionPresent = + fractionDigits > 0 && (minFractionDigits > 0 || fractionPart > 0); + + if (_hasIntegerDigits(integerDigits)) { + // Add the padding digits to the regular digits so that we get grouping. + var padding = '0' * (_minimumIntegerDigits - digitLength); + integerDigits = '$padding$integerDigits'; + digitLength = integerDigits.length; + for (var i = 0; i < digitLength; i++) { + _addDigit(integerDigits.codeUnitAt(i)); + } + } else if (!fractionPresent) { + // If neither fraction nor integer part exists, just print zero. + _addZero(); + } + + _decimalSeparator(fractionPresent); + if (fractionPresent) { + _formatFractionPart((fractionPart + power).toString(), minFractionDigits); + } + } + + /// Helper to get the floor of a number which might not be num. This should + /// only ever be called with an argument which is positive, or whose abs() + /// is negative. The second case is the maximum negative value on a + /// fixed-length integer. Since they are integers, they are also their own + /// floor. + dynamic _floor(dynamic number) { + if (number.isNegative && !number.abs().isNegative) { + throw ArgumentError( + 'Internal error: expected positive number, got $number'); + } + return (number is num) ? number.floor() : number ~/ 1; + } + + /// Helper to round a number which might not be num. + dynamic _round(dynamic number) { + if (number is num) { + if (number.isInfinite) { + return _maxInt; + } else { + return number.round(); + } + } else if (number.remainder(1) == 0) { + // Not a normal number, but int-like, e.g. Int64 + return number; + } else { + // TODO(alanknight): Do this more efficiently. If IntX had floor and + // round we could avoid this. + var basic = _floor(number); + var fraction = (number - basic).toDouble().round(); + return fraction == 0 ? number : number + fraction; + } + } + + // Return the number of digits left of the decimal place in [number]. + static int _numberOfIntegerDigits(dynamic number) { + var simpleNumber = (number.toDouble() as double).abs(); + // It's unfortunate that we have to do this, but we get precision errors + // that affect the result if we use logs, e.g. 1000000 + if (simpleNumber < 10) return 1; + if (simpleNumber < 100) return 2; + if (simpleNumber < 1000) return 3; + if (simpleNumber < 10000) return 4; + if (simpleNumber < 100000) return 5; + if (simpleNumber < 1000000) return 6; + if (simpleNumber < 10000000) return 7; + if (simpleNumber < 100000000) return 8; + if (simpleNumber < 1000000000) return 9; + if (simpleNumber < 10000000000) return 10; + if (simpleNumber < 100000000000) return 11; + if (simpleNumber < 1000000000000) return 12; + if (simpleNumber < 10000000000000) return 13; + if (simpleNumber < 100000000000000) return 14; + if (simpleNumber < 1000000000000000) return 15; + if (simpleNumber < 10000000000000000) return 16; + if (simpleNumber < 100000000000000000) return 17; + if (simpleNumber < 1000000000000000000) return 18; + return 19; + } + + /// Compute the raw integer digits which will then be printed with + /// grouping and translated to localized digits. + String _integerDigits(integerPart, extraIntegerDigits) { + // If the integer part is larger than the maximum integer size + // (2^52 on Javascript, 2^63 on the VM) it will lose precision, + // so pad out the rest of it with zeros. + var paddingDigits = ''; + if (integerPart is num && integerPart > _maxInt) { + var howManyDigitsTooBig = + (log(integerPart) / log(10)).ceil() - _maxDigits; + num divisor = pow(10, howManyDigitsTooBig).round(); + // pow() produces 0 if the result is too large for a 64-bit int. + // If that happens, use a floating point divisor instead. + if (divisor == 0) divisor = pow(10.0, howManyDigitsTooBig); + paddingDigits = '0' * howManyDigitsTooBig.toInt(); + integerPart = (integerPart / divisor).truncate(); + } + + var extra = extraIntegerDigits == 0 ? '' : extraIntegerDigits.toString(); + var intDigits = _mainIntegerDigits(integerPart); + var paddedExtra = intDigits.isEmpty ? extra : extra.padLeft(0, '0'); + return '$intDigits$paddedExtra$paddingDigits'; + } + + /// The digit string of the integer part. This is the empty string if the + /// integer part is zero and otherwise is the toString() of the integer + /// part, stripping off any minus sign. + String _mainIntegerDigits(integer) { + if (integer == 0) return ''; + var digits = integer.toString(); + // If we have a fixed-length int representation, it can have a negative + // number whose negation is also negative, e.g. 2^-63 in 64-bit. + // Remove the minus sign. + return digits.startsWith('-') ? digits.substring(1) : digits; + } + + /// Format the part after the decimal place in a fixed point number. + void _formatFractionPart(String fractionPart, int minDigits) { + var fractionLength = fractionPart.length; + while (fractionPart.codeUnitAt(fractionLength - 1) == '0'.codeUnitAt(0) && + fractionLength > minDigits + 1) { + fractionLength--; + } + for (var i = 1; i < fractionLength; i++) { + _addDigit(fractionPart.codeUnitAt(i)); + } + } + + /// Print the decimal separator if appropriate. + void _decimalSeparator(bool fractionPresent) { + if (fractionPresent) { + _add(_symbols.DECIMAL_SEP); + } + } + + /// Return true if we have a main integer part which is printable, either + /// because we have digits left of the decimal point (this may include digits + /// which have been moved left because of percent or permille formatting), + /// or because the minimum number of printable digits is greater than 1. + bool _hasIntegerDigits(String digits) => + digits.isNotEmpty || _minimumIntegerDigits > 0; + + /// A group of methods that provide support for writing digits and other + /// required characters into [_buffer] easily. + void _add(String x) { + _buffer.write(x); + } + + void _addZero() { + _buffer.write(_symbols.ZERO_DIGIT); + } + + void _addDigit(int x) { + _buffer.writeCharCode(x + _zeroOffset); + } +} + +// Suppress naming issues as changes would be breaking. +// ignore_for_file: non_constant_identifier_names +class _NumberSymbols { + final String DECIMAL_SEP, ZERO_DIGIT; + + const _NumberSymbols({required this.DECIMAL_SEP, required this.ZERO_DIGIT}); +} diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index 439f9444c4..1599bdbf6f 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -16,7 +16,6 @@ dependencies: meta: ^1.3.0 stack_trace: ^1.10.0 uuid: ^3.0.0 - intl: '>=0.17.0 <1.0.0' dev_dependencies: mockito: ^5.1.0 @@ -25,3 +24,4 @@ dev_dependencies: yaml: ^3.1.0 # needed for version match (code and pubspec) collection: ^1.16.0 coverage: ^1.3.0 + intl: '>=0.17.0 <1.0.0' diff --git a/dart/test/utils/sample_rate_format_test.dart b/dart/test/utils/sample_rate_format_test.dart new file mode 100644 index 0000000000..a9b4c80b09 --- /dev/null +++ b/dart/test/utils/sample_rate_format_test.dart @@ -0,0 +1,49 @@ +import 'package:sentry/src/utils/sample_rate_format.dart'; +import 'package:test/test.dart'; +import 'package:intl/intl.dart'; + +void main() { + test('format', () { + final inputs = [ + 0.0, + 1.0, + 0.1, + 0.11, + 0.19, + 0.191, + 0.1919, + 0.19191, + 0.191919, + 0.1919191, + 0.19191919, + 0.191919191, + 0.1919191919, + 0.19191919191, + 0.191919191919, + 0.1919191919191, + 0.19191919191919, + 0.191919191919191, + 0.1919191919191919, + 0.19191919191919199, + ]; + + for (final input in inputs) { + expect( + SampleRateFormat().format(input), + NumberFormat('#.################').format(input), + ); + } + }); + + test('input smaller 0 is capped', () { + expect(SampleRateFormat().format(-1), '0'); + }); + + test('input larger 1 is capped', () { + expect(SampleRateFormat().format(1.1), '1'); + }); + + test('call with NaN returns 0', () { + expect(SampleRateFormat().format(double.nan), '0'); + }); +} From 24f71aae695d5763d7ad28592191d95841f59b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 27 Mar 2023 11:51:44 +0000 Subject: [PATCH 9/9] Sanitize sensitive data from URLs (span desc, span data, crumbs, client errors) (#1327) Co-authored-by: Manoel Aranda Neto Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> --- CHANGELOG.md | 4 + dart/lib/sentry.dart | 5 + dart/lib/sentry_io.dart | 1 + .../web_enricher_event_processor.dart | 10 +- .../src/http_client/breadcrumb_client.dart | 9 +- dart/lib/src/http_client/tracing_client.dart | 16 +- dart/lib/src/protocol/breadcrumb.dart | 6 +- dart/lib/src/protocol/sentry_request.dart | 25 +-- dart/lib/src/utils/http_sanitizer.dart | 105 ++++++++++ dart/lib/src/utils/url_details.dart | 29 +++ .../enricher/web_enricher_test.dart | 17 ++ .../http_client/breadcrumb_client_test.dart | 26 ++- .../failed_request_client_test.dart | 26 ++- .../test/http_client/tracing_client_test.dart | 8 +- dart/test/protocol/breadcrumb_test.dart | 23 ++- dart/test/utils/http_sanitizer_test.dart | 179 ++++++++++++++++++ dart/test/utils/url_details_test.dart | 80 ++++++++ dio/lib/src/breadcrumb_client_adapter.dart | 8 +- dio/lib/src/sentry_transformer.dart | 26 ++- dio/lib/src/tracing_client_adapter.dart | 15 +- dio/test/breadcrumb_client_adapter_test.dart | 49 +++-- dio/test/dio_event_processor_test.dart | 25 +++ dio/test/sentry_transformer_test.dart | 38 +++- dio/test/tracing_client_adapter_test.dart | 10 +- flutter/lib/sentry_flutter.dart | 1 + 25 files changed, 657 insertions(+), 84 deletions(-) create mode 100644 dart/lib/src/utils/http_sanitizer.dart create mode 100644 dart/lib/src/utils/url_details.dart create mode 100644 dart/test/utils/http_sanitizer_test.dart create mode 100644 dart/test/utils/url_details_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 358bf15eb4..fc3c2ec75d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Sanitize sensitive data from URLs (span desc, span data, crumbs, client errors) ([#1327](https://github.com/getsentry/sentry-dart/pull/1327)) + ### Dependencies - Bump Cocoa SDK from v8.3.1 to v8.3.3 ([#1350](https://github.com/getsentry/sentry-dart/pull/1350), [#1355](https://github.com/getsentry/sentry-dart/pull/1355)) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 005941073f..3fee10e6ea 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -39,3 +39,8 @@ export 'src/exception_stacktrace_extractor.dart'; // Isolates export 'src/sentry_isolate_extension.dart'; export 'src/sentry_isolate.dart'; +// URL +// ignore: invalid_export_of_internal_element +export 'src/utils/http_sanitizer.dart'; +// ignore: invalid_export_of_internal_element +export 'src/utils/url_details.dart'; diff --git a/dart/lib/sentry_io.dart b/dart/lib/sentry_io.dart index db73e953ba..378baa2e2a 100644 --- a/dart/lib/sentry_io.dart +++ b/dart/lib/sentry_io.dart @@ -1,2 +1,3 @@ +// ignore: invalid_export_of_internal_element export 'sentry.dart'; export 'src/sentry_attachment/io_sentry_attachment.dart'; diff --git a/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart b/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart index be91d1e362..7aaa29b788 100644 --- a/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart +++ b/dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart @@ -49,10 +49,12 @@ class WebEnricherEventProcessor implements EnricherEventProcessor { header.putIfAbsent('User-Agent', () => _window.navigator.userAgent); - return (request ?? SentryRequest()).copyWith( - url: request?.url ?? _window.location.toString(), - headers: header, - ); + final url = request?.url ?? _window.location.toString(); + return (request ?? SentryRequest(url: url)) + .copyWith( + headers: header, + ) + .sanitized(); } SentryDevice _getDevice(SentryDevice? device) { diff --git a/dart/lib/src/http_client/breadcrumb_client.dart b/dart/lib/src/http_client/breadcrumb_client.dart index 4a6dd12920..803a3e18a0 100644 --- a/dart/lib/src/http_client/breadcrumb_client.dart +++ b/dart/lib/src/http_client/breadcrumb_client.dart @@ -2,6 +2,8 @@ import 'package:http/http.dart'; import '../protocol.dart'; import '../hub.dart'; import '../hub_adapter.dart'; +import '../utils/url_details.dart'; +import '../utils/http_sanitizer.dart'; /// A [http](https://pub.dev/packages/http)-package compatible HTTP client /// which records requests as breadcrumbs. @@ -75,15 +77,20 @@ class BreadcrumbClient extends BaseClient { } finally { stopwatch.stop(); + final urlDetails = + HttpSanitizer.sanitizeUrl(request.url.toString()) ?? UrlDetails(); + var breadcrumb = Breadcrumb.http( level: requestHadException ? SentryLevel.error : SentryLevel.info, - url: request.url, + url: Uri.parse(urlDetails.urlOrFallback), method: request.method, statusCode: statusCode, reason: reason, requestDuration: stopwatch.elapsed, requestBodySize: request.contentLength, responseBodySize: responseBodySize, + httpQuery: urlDetails.query, + httpFragment: urlDetails.fragment, ); await _hub.addBreadcrumb(breadcrumb); diff --git a/dart/lib/src/http_client/tracing_client.dart b/dart/lib/src/http_client/tracing_client.dart index cd55198a75..1b117f9eda 100644 --- a/dart/lib/src/http_client/tracing_client.dart +++ b/dart/lib/src/http_client/tracing_client.dart @@ -4,6 +4,7 @@ import '../hub_adapter.dart'; import '../protocol.dart'; import '../tracing.dart'; import '../utils/tracing_utils.dart'; +import '../utils/http_sanitizer.dart'; /// A [http](https://pub.dev/packages/http)-package compatible HTTP client /// which adds support to Sentry Performance feature. @@ -19,17 +20,28 @@ class TracingClient extends BaseClient { @override Future send(BaseRequest request) async { // see https://develop.sentry.dev/sdk/performance/#header-sentry-trace + + final urlDetails = HttpSanitizer.sanitizeUrl(request.url.toString()); + + var description = request.method; + if (urlDetails != null) { + description += ' ${urlDetails.urlOrFallback}'; + } + final currentSpan = _hub.getSpan(); var span = currentSpan?.startChild( 'http.client', - description: '${request.method} ${request.url}', + description: description, ); - // if the span is NoOp, we dont want to attach headers + // if the span is NoOp, we don't want to attach headers if (span is NoOpSentrySpan) { span = null; } + span?.setData('method', request.method); + urlDetails?.applyToSpan(span); + StreamedResponse? response; try { if (span != null) { diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index b25bc16176..f7eea55358 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -3,7 +3,7 @@ import 'package:meta/meta.dart'; import '../utils.dart'; import '../protocol.dart'; -/// Structed data to describe more information pior to the event captured. +/// Structured data to describe more information prior to the event captured. /// See `Sentry.captureEvent()`. /// /// The outgoing JSON representation is: @@ -47,6 +47,8 @@ class Breadcrumb { // Size of the response body in bytes int? responseBodySize, + String? httpQuery, + String? httpFragment, }) { return Breadcrumb( type: 'http', @@ -61,6 +63,8 @@ class Breadcrumb { if (requestDuration != null) 'duration': requestDuration.toString(), if (requestBodySize != null) 'request_body_size': requestBodySize, if (responseBodySize != null) 'response_body_size': responseBodySize, + if (httpQuery != null) 'http.query': httpQuery, + if (httpFragment != null) 'http.fragment': httpFragment, }, ); } diff --git a/dart/lib/src/protocol/sentry_request.dart b/dart/lib/src/protocol/sentry_request.dart index d4d44766fc..f6c371bfa1 100644 --- a/dart/lib/src/protocol/sentry_request.dart +++ b/dart/lib/src/protocol/sentry_request.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import '../utils/iterable_extension.dart'; +import '../utils/http_sanitizer.dart'; /// The Request interface contains information on a HTTP request related to the event. /// In client SDKs, this can be an outgoing request, or the request that rendered the current web page. @@ -92,31 +93,18 @@ class SentryRequest { @Deprecated('Will be removed in v8. Use [data] instead') Map? other, }) { - // As far as I can tell there's no way to get the uri without the query part - // so we replace it with an empty string. - final urlWithoutQuery = uri - .replace(query: '', fragment: '') - .toString() - .replaceAll('?', '') - .replaceAll('#', ''); - - // Future proof, Dio does not support it yet and even if passing in the path, - // the parsing of the uri returns empty. - final query = uri.query.isEmpty ? null : uri.query; - final fragment = uri.fragment.isEmpty ? null : uri.fragment; - return SentryRequest( - url: urlWithoutQuery, - fragment: fragment, - queryString: query, + url: uri.toString(), method: method, cookies: cookies, data: data, headers: headers, env: env, + queryString: uri.query, + fragment: uri.fragment, // ignore: deprecated_member_use_from_same_package other: other, - ); + ).sanitized(); } /// Deserializes a [SentryRequest] from JSON [Map]. @@ -162,12 +150,13 @@ class SentryRequest { Map? env, @Deprecated('Will be removed in v8. Use [data] instead') Map? other, + bool removeCookies = false, }) => SentryRequest( url: url ?? this.url, method: method ?? this.method, queryString: queryString ?? this.queryString, - cookies: cookies ?? this.cookies, + cookies: removeCookies ? null : cookies ?? this.cookies, data: data ?? _data, headers: headers ?? _headers, env: env ?? _env, diff --git a/dart/lib/src/utils/http_sanitizer.dart b/dart/lib/src/utils/http_sanitizer.dart new file mode 100644 index 0000000000..7a2d391c20 --- /dev/null +++ b/dart/lib/src/utils/http_sanitizer.dart @@ -0,0 +1,105 @@ +import 'package:meta/meta.dart'; + +import '../protocol.dart'; +import 'url_details.dart'; + +@internal +class HttpSanitizer { + static final RegExp _authRegExp = RegExp("(.+://)(.*@)(.*)"); + static final List _securityHeaders = [ + "X-FORWARDED-FOR", + "AUTHORIZATION", + "COOKIE", + "SET-COOKIE", + "X-API-KEY", + "X-REAL-IP", + "REMOTE-ADDR", + "FORWARDED", + "PROXY-AUTHORIZATION", + "X-CSRF-TOKEN", + "X-CSRFTOKEN", + "X-XSRF-TOKEN" + ]; + + /// Parse and sanitize url data for sentry.io + static UrlDetails? sanitizeUrl(String? url) { + if (url == null) { + return null; + } + + final queryIndex = url.indexOf('?'); + final fragmentIndex = url.indexOf('#'); + + if (queryIndex > -1 && fragmentIndex > -1 && fragmentIndex < queryIndex) { + // url considered malformed because of fragment position + return UrlDetails(); + } else { + try { + final uri = Uri.parse(url); + final urlWithAuthRemoved = _urlWithAuthRemoved(uri._url()); + return UrlDetails( + url: urlWithAuthRemoved.isEmpty ? null : urlWithAuthRemoved, + query: uri.query.isEmpty ? null : uri.query, + fragment: uri.fragment.isEmpty ? null : uri.fragment); + } catch (_) { + return null; + } + } + } + + static Map? sanitizedHeaders(Map? headers) { + if (headers == null) { + return null; + } + final sanitizedHeaders = {}; + headers.forEach((key, value) { + if (!_securityHeaders.contains(key.toUpperCase())) { + sanitizedHeaders[key] = value; + } + }); + return sanitizedHeaders; + } + + static String _urlWithAuthRemoved(String url) { + final userInfoMatch = _authRegExp.firstMatch(url); + if (userInfoMatch != null && userInfoMatch.groupCount == 3) { + final userInfoString = userInfoMatch.group(2) ?? ''; + final replacementString = userInfoString.contains(":") + ? "[Filtered]:[Filtered]@" + : "[Filtered]@"; + return '${userInfoMatch.group(1) ?? ''}$replacementString${userInfoMatch.group(3) ?? ''}'; + } else { + return url; + } + } +} + +extension UriPath on Uri { + String _url() { + var buffer = ''; + if (scheme.isNotEmpty) { + buffer += '$scheme://'; + } + if (userInfo.isNotEmpty) { + buffer += '$userInfo@'; + } + buffer += host; + if (path.isNotEmpty) { + buffer += path; + } + return buffer; + } +} + +extension SanitizedSentryRequest on SentryRequest { + SentryRequest sanitized() { + final urlDetails = HttpSanitizer.sanitizeUrl(url) ?? UrlDetails(); + return copyWith( + url: urlDetails.urlOrFallback, + queryString: urlDetails.query, + fragment: urlDetails.fragment, + headers: HttpSanitizer.sanitizedHeaders(headers), + removeCookies: true, + ); + } +} diff --git a/dart/lib/src/utils/url_details.dart b/dart/lib/src/utils/url_details.dart new file mode 100644 index 0000000000..89022a7892 --- /dev/null +++ b/dart/lib/src/utils/url_details.dart @@ -0,0 +1,29 @@ +import 'package:meta/meta.dart'; +import '../../sentry.dart'; + +/// Sanitized url data for sentry.io +@internal +class UrlDetails { + UrlDetails({this.url, this.query, this.fragment}); + + final String? url; + final String? query; + final String? fragment; + + late final urlOrFallback = url ?? 'unknown'; + + void applyToSpan(ISentrySpan? span) { + if (span == null) { + return; + } + if (url != null) { + span.setData('url', url); + } + if (query != null) { + span.setData("http.query", query); + } + if (fragment != null) { + span.setData("http.fragment", fragment); + } + } +} diff --git a/dart/test/event_processor/enricher/web_enricher_test.dart b/dart/test/event_processor/enricher/web_enricher_test.dart index be44614345..d4aa14b61f 100644 --- a/dart/test/event_processor/enricher/web_enricher_test.dart +++ b/dart/test/event_processor/enricher/web_enricher_test.dart @@ -57,6 +57,23 @@ void main() { expect(event.request?.url, 'foo.bar'); }); + test('does not add auth headers to request', () async { + var event = SentryEvent( + request: SentryRequest( + url: 'foo.bar', + headers: { + 'Authorization': 'foo', + 'authorization': 'bar', + }, + ), + ); + var enricher = fixture.getSut(); + event = await enricher.apply(event); + + expect(event.request?.headers['Authorization'], isNull); + expect(event.request?.headers['authorization'], isNull); + }); + test('user-agent is not overridden if already present', () async { var event = SentryEvent( request: SentryRequest( diff --git a/dart/test/http_client/breadcrumb_client_test.dart b/dart/test/http_client/breadcrumb_client_test.dart index da7e1b4b7b..1b7ae91bfa 100644 --- a/dart/test/http_client/breadcrumb_client_test.dart +++ b/dart/test/http_client/breadcrumb_client_test.dart @@ -9,7 +9,7 @@ import 'package:test/test.dart'; import '../mocks/mock_hub.dart'; -final requestUri = Uri.parse('https://example.com'); +final requestUri = Uri.parse('https://example.com/path?foo=bar#baz'); void main() { group(BreadcrumbClient, () { @@ -30,8 +30,10 @@ void main() { final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com'); + expect(breadcrumb.data?['url'], 'https://example.com/path'); expect(breadcrumb.data?['method'], 'GET'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['reason'], 'OK'); expect(breadcrumb.data?['duration'], isNotNull); @@ -51,8 +53,10 @@ void main() { final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com'); + expect(breadcrumb.data?['url'], 'https://example.com/path'); expect(breadcrumb.data?['method'], 'GET'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 404); expect(breadcrumb.data?['reason'], 'NOT FOUND'); expect(breadcrumb.data?['duration'], isNotNull); @@ -68,8 +72,10 @@ void main() { final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com'); + expect(breadcrumb.data?['url'], 'https://example.com/path'); expect(breadcrumb.data?['method'], 'POST'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['duration'], isNotNull); }); @@ -84,8 +90,10 @@ void main() { final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com'); + expect(breadcrumb.data?['url'], 'https://example.com/path'); expect(breadcrumb.data?['method'], 'PUT'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['duration'], isNotNull); }); @@ -100,8 +108,10 @@ void main() { final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com'); + expect(breadcrumb.data?['url'], 'https://example.com/path'); expect(breadcrumb.data?['method'], 'DELETE'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['duration'], isNotNull); }); @@ -160,8 +170,10 @@ void main() { final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com'); + expect(breadcrumb.data?['url'], 'https://example.com/path'); expect(breadcrumb.data?['method'], 'GET'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.level, SentryLevel.error); expect(breadcrumb.data?['duration'], isNotNull); }); diff --git a/dart/test/http_client/failed_request_client_test.dart b/dart/test/http_client/failed_request_client_test.dart index 2e28a0ff61..d7b3b4dba8 100644 --- a/dart/test/http_client/failed_request_client_test.dart +++ b/dart/test/http_client/failed_request_client_test.dart @@ -59,8 +59,8 @@ void main() { expect(request?.url, 'https://example.com'); expect(request?.queryString, 'foo=bar'); expect(request?.fragment, 'myFragment'); - expect(request?.cookies, 'foo=bar'); - expect(request?.headers, {'Cookie': 'foo=bar'}); + expect(request?.cookies, isNull); + expect(request?.headers, {}); // ignore: deprecated_member_use_from_same_package expect(request?.other.keys.contains('duration'), true); // ignore: deprecated_member_use_from_same_package @@ -134,8 +134,8 @@ void main() { expect(request?.url, 'https://example.com'); expect(request?.queryString, 'foo=bar'); expect(request?.fragment, 'myFragment'); - expect(request?.cookies, 'foo=bar'); - expect(request?.headers, {'Cookie': 'foo=bar'}); + expect(request?.cookies, isNull); + expect(request?.headers, {}); // ignore: deprecated_member_use_from_same_package expect(request?.other.keys.contains('duration'), true); // ignore: deprecated_member_use_from_same_package @@ -195,6 +195,24 @@ void main() { expect(event.contexts.response, isNull); }); + test('removes authorization headers', () async { + fixture._hub.options.captureFailedRequests = true; + final sut = fixture.getSut( + client: createThrowingClient(), + ); + + await expectLater( + () async => await sut.get(requestUri, + headers: {'authorization': 'foo', 'Authorization': 'foo'}), + throwsException, + ); + + final event = fixture.transport.events.first; + expect(fixture.transport.calls, 1); + expect(event.request, isNotNull); + expect(event.request?.headers.isEmpty, true); + }); + test('pii is not send on invalid status code', () async { final sut = fixture.getSut( client: fixture.getClient(statusCode: 404), diff --git a/dart/test/http_client/tracing_client_test.dart b/dart/test/http_client/tracing_client_test.dart index dceca47961..0b175174cf 100644 --- a/dart/test/http_client/tracing_client_test.dart +++ b/dart/test/http_client/tracing_client_test.dart @@ -8,7 +8,7 @@ import 'package:test/test.dart'; import '../mocks.dart'; import '../mocks/mock_transport.dart'; -final requestUri = Uri.parse('https://example.com?foo=bar'); +final requestUri = Uri.parse('https://example.com?foo=bar#baz'); void main() { group(TracingClient, () { @@ -37,7 +37,11 @@ void main() { expect(span.status, SpanStatus.ok()); expect(span.context.operation, 'http.client'); - expect(span.context.description, 'GET https://example.com?foo=bar'); + expect(span.context.description, 'GET https://example.com'); + expect(span.data['method'], 'GET'); + expect(span.data['url'], 'https://example.com'); + expect(span.data['http.query'], 'foo=bar'); + expect(span.data['http.fragment'], 'baz'); }); test('finish span if errored request', () async { diff --git a/dart/test/protocol/breadcrumb_test.dart b/dart/test/protocol/breadcrumb_test.dart index 35ab621de4..4062e1b895 100644 --- a/dart/test/protocol/breadcrumb_test.dart +++ b/dart/test/protocol/breadcrumb_test.dart @@ -82,16 +82,17 @@ void main() { group('ctor', () { test('Breadcrumb http', () { final breadcrumb = Breadcrumb.http( - url: Uri.parse('https://example.org'), - method: 'GET', - level: SentryLevel.fatal, - reason: 'OK', - statusCode: 200, - requestDuration: Duration.zero, - timestamp: DateTime.now(), - requestBodySize: 2, - responseBodySize: 3, - ); + url: Uri.parse('https://example.org'), + method: 'GET', + level: SentryLevel.fatal, + reason: 'OK', + statusCode: 200, + requestDuration: Duration.zero, + timestamp: DateTime.now(), + requestBodySize: 2, + responseBodySize: 3, + httpQuery: 'foo=bar', + httpFragment: 'baz'); final json = breadcrumb.toJson(); expect(json, { @@ -106,6 +107,8 @@ void main() { 'duration': '0:00:00.000000', 'request_body_size': 2, 'response_body_size': 3, + 'http.query': 'foo=bar', + 'http.fragment': 'baz' }, 'level': 'fatal', 'type': 'http', diff --git a/dart/test/utils/http_sanitizer_test.dart b/dart/test/utils/http_sanitizer_test.dart new file mode 100644 index 0000000000..176d64e51f --- /dev/null +++ b/dart/test/utils/http_sanitizer_test.dart @@ -0,0 +1,179 @@ +import 'package:sentry/src/utils/http_sanitizer.dart'; +import 'package:test/test.dart'; + +void main() { + test('returns null for null', () { + expect(HttpSanitizer.sanitizeUrl(null), isNull); + }); + + test('strips user info with user and password from http', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl( + "http://user:password@sentry.io?q=1&s=2&token=secret#top"); + expect(sanitizedUri?.url, "http://[Filtered]:[Filtered]@sentry.io"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, "top"); + }); + + test('strips user info with user and password from https', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl( + "https://user:password@sentry.io?q=1&s=2&token=secret#top"); + expect(sanitizedUri?.url, "https://[Filtered]:[Filtered]@sentry.io"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, "top"); + }); + + test('splits url', () { + final sanitizedUri = + HttpSanitizer.sanitizeUrl("https://sentry.io?q=1&s=2&token=secret#top"); + expect(sanitizedUri?.url, "https://sentry.io"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, "top"); + }); + + test('splits relative url', () { + final sanitizedUri = + HttpSanitizer.sanitizeUrl("/users/1?q=1&s=2&token=secret#top"); + expect(sanitizedUri?.url, "/users/1"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, "top"); + }); + + test('splits relative root url', () { + final sanitizedUri = + HttpSanitizer.sanitizeUrl("/?q=1&s=2&token=secret#top"); + expect(sanitizedUri?.url, "/"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, "top"); + }); + + test('splits url with just query and fragment', () { + final sanitizedUri = + HttpSanitizer.sanitizeUrl("/?q=1&s=2&token=secret#top"); + expect(sanitizedUri?.url, "/"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, "top"); + }); + + test('splits relative url with query only', () { + final sanitizedUri = + HttpSanitizer.sanitizeUrl("/users/1?q=1&s=2&token=secret"); + expect(sanitizedUri?.url, "/users/1"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, isNull); + }); + + test('splits relative url with fragment only', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl("/users/1#top"); + expect(sanitizedUri?.url, "/users/1"); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, "top"); + }); + + test('strips user info with user and password without query', () { + final sanitizedUri = + HttpSanitizer.sanitizeUrl("https://user:password@sentry.io#top"); + expect(sanitizedUri?.url, "https://[Filtered]:[Filtered]@sentry.io"); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, "top"); + }); + + test('splits without query', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl("https://sentry.io#top"); + expect(sanitizedUri?.url, "https://sentry.io"); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, "top"); + }); + + test('strips user info with user and password without fragment', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl( + "https://user:password@sentry.io?q=1&s=2&token=secret"); + expect(sanitizedUri?.url, "https://[Filtered]:[Filtered]@sentry.io"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, isNull); + }); + + test('strips user info with user and password without query or fragment', () { + final sanitizedUri = + HttpSanitizer.sanitizeUrl("https://user:password@sentry.io"); + expect(sanitizedUri?.url, "https://[Filtered]:[Filtered]@sentry.io"); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, isNull); + }); + + test('splits url without query or fragment and no authority', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl("https://sentry.io"); + expect(sanitizedUri?.url, "https://sentry.io"); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, isNull); + }); + + test('strips user info with user only', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl( + "https://user@sentry.io?q=1&s=2&token=secret#top"); + expect(sanitizedUri?.url, "https://[Filtered]@sentry.io"); + expect(sanitizedUri?.query, "q=1&s=2&token=secret"); + expect(sanitizedUri?.fragment, "top"); + }); + + test('no details extracted with query after fragment', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl( + "https://user:password@sentry.io#fragment?q=1&s=2&token=secret"); + expect(sanitizedUri?.url, isNull); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, isNull); + }); + + test('no details extracted with query after fragment without authority', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl( + "https://sentry.io#fragment?q=1&s=2&token=secret"); + expect(sanitizedUri?.url, isNull); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, isNull); + }); + + test('no details extracted from malformed url', () { + final sanitizedUri = HttpSanitizer.sanitizeUrl( + "htps://user@sentry.io#fragment?q=1&s=2&token=secret"); + expect(sanitizedUri?.url, isNull); + expect(sanitizedUri?.query, isNull); + expect(sanitizedUri?.fragment, isNull); + }); + + test('removes security headers', () { + final securityHeaders = [ + "X-FORWARDED-FOR", + "AUTHORIZATION", + "COOKIE", + "SET-COOKIE", + "X-API-KEY", + "X-REAL-IP", + "REMOTE-ADDR", + "FORWARDED", + "PROXY-AUTHORIZATION", + "X-CSRF-TOKEN", + "X-CSRFTOKEN", + "X-XSRF-TOKEN" + ]; + + final headers = {}; + for (final securityHeader in securityHeaders) { + headers[securityHeader] = 'foo'; + headers[securityHeader.toLowerCase()] = 'bar'; + headers[securityHeader._capitalize()] = 'baz'; + } + final sanitizedHeaders = HttpSanitizer.sanitizedHeaders(headers); + expect(sanitizedHeaders, isNotNull); + expect(sanitizedHeaders?.isEmpty, true); + }); + + test('handle throwing uri', () { + final details = HttpSanitizer.sanitizeUrl('::Not valid URI::'); + expect(details, isNull); + }); +} + +extension StringExtension on String { + String _capitalize() { + return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; + } +} diff --git a/dart/test/utils/url_details_test.dart b/dart/test/utils/url_details_test.dart new file mode 100644 index 0000000000..b667bde972 --- /dev/null +++ b/dart/test/utils/url_details_test.dart @@ -0,0 +1,80 @@ +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + test('does not crash on null span', () { + final urlDetails = + UrlDetails(url: "https://sentry.io/api", query: "q=1", fragment: "top"); + urlDetails.applyToSpan(null); + }); + + test('applies all to span', () { + final urlDetails = + UrlDetails(url: "https://sentry.io/api", query: "q=1", fragment: "top"); + final span = MockSpan(); + urlDetails.applyToSpan(span); + + verify(span.setData("url", "https://sentry.io/api")); + verify(span.setData("http.query", "q=1")); + verify(span.setData("http.fragment", "top")); + }); + + test('applies only url to span', () { + final urlDetails = UrlDetails(url: "https://sentry.io/api"); + final span = MockSpan(); + urlDetails.applyToSpan(span); + + verify(span.setData("url", "https://sentry.io/api")); + verifyNoMoreInteractions(span); + }); + + test('applies only query to span', () { + final urlDetails = UrlDetails(query: "q=1"); + final span = MockSpan(); + urlDetails.applyToSpan(span); + + verify(span.setData("http.query", "q=1")); + verifyNoMoreInteractions(span); + }); + + test('applies only fragment to span', () { + final urlDetails = UrlDetails(fragment: "top"); + final span = MockSpan(); + urlDetails.applyToSpan(span); + + verify(span.setData("http.fragment", "top")); + verifyNoMoreInteractions(span); + }); + + test('applies details to request', () { + final url = "https://sentry.io/api?q=1#top"; + final request = SentryRequest(url: url).sanitized(); + + expect(request.url, "https://sentry.io/api"); + expect(request.queryString, "q=1"); + expect(request.fragment, "top"); + }); + + test('applies details without fragment and url to request', () { + final request = SentryRequest(url: 'https://sentry.io/api').sanitized(); + + expect(request.url, "https://sentry.io/api"); + expect(request.queryString, isNull); + expect(request.fragment, isNull); + }); + + test('removes cookies from request', () { + final request = + SentryRequest(url: 'https://sentry.io/api', cookies: 'foo=bar') + .sanitized(); + expect(request.cookies, isNull); + }); + + test('returns fallback for null URL', () { + final urlDetails = UrlDetails(url: null); + expect(urlDetails.urlOrFallback, "unknown"); + }); +} + +class MockSpan extends Mock implements SentrySpan {} diff --git a/dio/lib/src/breadcrumb_client_adapter.dart b/dio/lib/src/breadcrumb_client_adapter.dart index 17233d984e..a2686031d4 100644 --- a/dio/lib/src/breadcrumb_client_adapter.dart +++ b/dio/lib/src/breadcrumb_client_adapter.dart @@ -55,14 +55,20 @@ class BreadcrumbClientAdapter implements HttpClientAdapter { } finally { stopwatch.stop(); + final urlDetails = + // ignore: invalid_use_of_internal_member + HttpSanitizer.sanitizeUrl(options.uri.toString()) ?? UrlDetails(); + final breadcrumb = Breadcrumb.http( level: requestHadException ? SentryLevel.error : SentryLevel.info, - url: options.uri, + url: Uri.parse(urlDetails.urlOrFallback), method: options.method, statusCode: statusCode, reason: reason, requestDuration: stopwatch.elapsed, responseBodySize: responseBodySize, + httpQuery: urlDetails.query, + httpFragment: urlDetails.fragment, ); await _hub.addBreadcrumb(breadcrumb); diff --git a/dio/lib/src/sentry_transformer.dart b/dio/lib/src/sentry_transformer.dart index ae77084405..eaf6c49d00 100644 --- a/dio/lib/src/sentry_transformer.dart +++ b/dio/lib/src/sentry_transformer.dart @@ -15,10 +15,21 @@ class SentryTransformer implements Transformer { @override Future transformRequest(RequestOptions options) async { + // ignore: invalid_use_of_internal_member + final urlDetails = HttpSanitizer.sanitizeUrl(options.uri.toString()); + var description = options.method; + if (urlDetails != null) { + description += ' ${urlDetails.urlOrFallback}'; + } + final span = _hub.getSpan()?.startChild( _serializeOp, - description: '${options.method} ${options.uri}', + description: description, ); + + span?.setData('method', options.method); + urlDetails?.applyToSpan(span); + String? request; try { request = await _transformer.transformRequest(options); @@ -39,10 +50,21 @@ class SentryTransformer implements Transformer { RequestOptions options, ResponseBody response, ) async { + // ignore: invalid_use_of_internal_member + final urlDetails = HttpSanitizer.sanitizeUrl(options.uri.toString()); + var description = options.method; + if (urlDetails != null) { + description += ' ${urlDetails.urlOrFallback}'; + } + final span = _hub.getSpan()?.startChild( _serializeOp, - description: '${options.method} ${options.uri}', + description: description, ); + + span?.setData('method', options.method); + urlDetails?.applyToSpan(span); + dynamic transformedResponse; try { transformedResponse = diff --git a/dio/lib/src/tracing_client_adapter.dart b/dio/lib/src/tracing_client_adapter.dart index 8a5bf3966f..38f743f0f7 100644 --- a/dio/lib/src/tracing_client_adapter.dart +++ b/dio/lib/src/tracing_client_adapter.dart @@ -23,18 +23,29 @@ class TracingClientAdapter implements HttpClientAdapter { Stream? requestStream, Future? cancelFuture, ) async { + // ignore: invalid_use_of_internal_member + final urlDetails = HttpSanitizer.sanitizeUrl(options.uri.toString()); + + var description = options.method; + if (urlDetails != null) { + description += ' ${urlDetails.urlOrFallback}'; + } + // see https://develop.sentry.dev/sdk/performance/#header-sentry-trace final currentSpan = _hub.getSpan(); var span = currentSpan?.startChild( 'http.client', - description: '${options.method} ${options.uri}', + description: description, ); - // if the span is NoOp, we dont want to attach headers + // if the span is NoOp, we don't want to attach headers if (span is NoOpSentrySpan) { span = null; } + span?.setData('method', options.method); + urlDetails?.applyToSpan(span); + ResponseBody? response; try { if (span != null) { diff --git a/dio/test/breadcrumb_client_adapter_test.dart b/dio/test/breadcrumb_client_adapter_test.dart index 28fa4769df..24250f4824 100644 --- a/dio/test/breadcrumb_client_adapter_test.dart +++ b/dio/test/breadcrumb_client_adapter_test.dart @@ -19,15 +19,17 @@ void main() { final sut = fixture.getSut(fixture.getClient(statusCode: 200, reason: 'OK')); - final response = await sut.get('/'); + final response = await sut.get(''); expect(response.statusCode, 200); expect(fixture.hub.addBreadcrumbCalls.length, 1); final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com/'); + expect(breadcrumb.data?['url'], 'https://example.com'); expect(breadcrumb.data?['method'], 'GET'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['reason'], null); expect(breadcrumb.data?['duration'], isNotNull); @@ -38,15 +40,17 @@ void main() { test('POST: happy path', () async { final sut = fixture.getSut(fixture.getClient(statusCode: 200)); - final response = await sut.post('/'); + final response = await sut.post(''); expect(response.statusCode, 200); expect(fixture.hub.addBreadcrumbCalls.length, 1); final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com/'); + expect(breadcrumb.data?['url'], 'https://example.com'); expect(breadcrumb.data?['method'], 'POST'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['duration'], isNotNull); }); @@ -54,15 +58,17 @@ void main() { test('PUT: happy path', () async { final sut = fixture.getSut(fixture.getClient(statusCode: 200)); - final response = await sut.put('/'); + final response = await sut.put(''); expect(response.statusCode, 200); expect(fixture.hub.addBreadcrumbCalls.length, 1); final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com/'); + expect(breadcrumb.data?['url'], 'https://example.com'); expect(breadcrumb.data?['method'], 'PUT'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['duration'], isNotNull); }); @@ -70,15 +76,17 @@ void main() { test('DELETE: happy path', () async { final sut = fixture.getSut(fixture.getClient(statusCode: 200)); - final response = await sut.delete('/'); + final response = await sut.delete(''); expect(response.statusCode, 200); expect(fixture.hub.addBreadcrumbCalls.length, 1); final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com/'); + expect(breadcrumb.data?['url'], 'https://example.com'); expect(breadcrumb.data?['method'], 'DELETE'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.data?['status_code'], 200); expect(breadcrumb.data?['duration'], isNotNull); }); @@ -89,17 +97,20 @@ void main() { test('no captureException for Exception', () async { final sut = fixture.getSut( MockHttpClientAdapter((options, requestStream, cancelFuture) async { - expect(options.uri, Uri.parse('https://example.com/')); + expect(options.uri, Uri.parse('https://example.com?foo=bar#baz')); throw Exception('test'); }), ); try { - await sut.get('/'); + await sut.get(''); fail('Method did not throw'); } on DioError catch (e) { expect(e.error.toString(), 'Exception: test'); - expect(e.requestOptions.uri, Uri.parse('https://example.com/')); + expect( + e.requestOptions.uri, + Uri.parse('https://example.com?foo=bar#baz'), + ); } expect(fixture.hub.captureExceptionCalls.length, 0); @@ -108,13 +119,13 @@ void main() { test('breadcrumb gets added when an exception gets thrown', () async { final sut = fixture.getSut( MockHttpClientAdapter((options, requestStream, cancelFuture) async { - expect(options.uri, Uri.parse('https://example.com/')); + expect(options.uri, Uri.parse('https://example.com')); throw Exception('foo bar'); }), ); try { - await sut.get('/'); + await sut.get(''); fail('Method did not throw'); } on DioError catch (_) {} @@ -123,8 +134,10 @@ void main() { final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; expect(breadcrumb.type, 'http'); - expect(breadcrumb.data?['url'], 'https://example.com/'); + expect(breadcrumb.data?['url'], 'https://example.com'); expect(breadcrumb.data?['method'], 'GET'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); expect(breadcrumb.level, SentryLevel.error); expect(breadcrumb.data?['duration'], isNotNull); }); @@ -145,13 +158,13 @@ void main() { test('Breadcrumb has correct duration', () async { final sut = fixture.getSut( MockHttpClientAdapter((options, _, __) async { - expect(options.uri, Uri.parse('https://example.com/')); + expect(options.uri, Uri.parse('https://example.com?foo=bar#baz')); await Future.delayed(Duration(seconds: 1)); return ResponseBody.fromString('', 200); }), ); - final response = await sut.get('/'); + final response = await sut.get(''); expect(response.statusCode, 200); expect(fixture.hub.addBreadcrumbCalls.length, 1); @@ -170,7 +183,7 @@ class Fixture { Dio getSut([MockHttpClientAdapter? client]) { final mc = client ?? getClient(); final dio = Dio( - BaseOptions(baseUrl: 'https://example.com/'), + BaseOptions(baseUrl: 'https://example.com?foo=bar#baz'), ); dio.httpClientAdapter = BreadcrumbClientAdapter(client: mc, hub: hub); return dio; @@ -180,7 +193,7 @@ class Fixture { MockHttpClientAdapter getClient({int statusCode = 200, String? reason}) { return MockHttpClientAdapter((request, requestStream, cancelFuture) async { - expect(request.uri, Uri.parse('https://example.com/')); + expect(request.uri, Uri.parse('https://example.com?foo=bar#baz')); return ResponseBody.fromString( '', diff --git a/dio/test/dio_event_processor_test.dart b/dio/test/dio_event_processor_test.dart index 904d13b3cf..4c8c2c1945 100644 --- a/dio/test/dio_event_processor_test.dart +++ b/dio/test/dio_event_processor_test.dart @@ -105,6 +105,31 @@ void main() { expect(processedEvent.request?.data, null); expect(processedEvent.request?.headers, {}); }); + + test('$DioEventProcessor removes auth headers', () { + final sut = fixture.getSut(sendDefaultPii: false); + + final requestOptionsWithAuthHeaders = requestOptions.copyWith( + headers: {'authorization': 'foo', 'Authorization': 'bar'}, + ); + final throwable = Exception(); + final dioError = DioError( + requestOptions: requestOptionsWithAuthHeaders, + response: Response( + requestOptions: requestOptionsWithAuthHeaders, + ), + ); + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); + final processedEvent = sut.apply(event) as SentryEvent; + + expect(processedEvent.request?.headers, {}); + }); }); group('response', () { diff --git a/dio/test/sentry_transformer_test.dart b/dio/test/sentry_transformer_test.dart index 3e6a8c36e4..821b9a6beb 100644 --- a/dio/test/sentry_transformer_test.dart +++ b/dio/test/sentry_transformer_test.dart @@ -11,6 +11,8 @@ import 'mocks.dart'; import 'mocks/mock_transport.dart'; import 'package:sentry/src/sentry_tracer.dart'; +final requestUri = Uri.parse('https://example.com?foo=bar#baz'); + void main() { group(SentryTransformer, () { late Fixture fixture; @@ -18,6 +20,7 @@ void main() { setUp(() { fixture = Fixture(); }); + test('transformRequest creates span', () async { final sut = fixture.getSut(); final tr = fixture._hub.startTransaction( @@ -26,7 +29,7 @@ void main() { bindToScope: true, ); - await sut.transformRequest(RequestOptions(path: 'foo')); + await sut.transformRequest(RequestOptions(path: requestUri.toString())); await tr.finish(); @@ -35,7 +38,11 @@ void main() { expect(span.status, SpanStatus.ok()); expect(span.context.operation, 'serialize.http.client'); - expect(span.context.description, 'GET foo'); + expect(span.context.description, 'GET https://example.com'); + expect(span.data['method'], 'GET'); + expect(span.data['url'], 'https://example.com'); + expect(span.data['http.query'], 'foo=bar'); + expect(span.data['http.fragment'], 'baz'); }); test('transformRequest finish span if errored request', () async { @@ -47,7 +54,7 @@ void main() { ); try { - await sut.transformRequest(RequestOptions(path: 'foo')); + await sut.transformRequest(RequestOptions(path: requestUri.toString())); } catch (_) {} await tr.finish(); @@ -57,7 +64,11 @@ void main() { expect(span.status, SpanStatus.internalError()); expect(span.context.operation, 'serialize.http.client'); - expect(span.context.description, 'GET foo'); + expect(span.context.description, 'GET https://example.com'); + expect(span.data['method'], 'GET'); + expect(span.data['url'], 'https://example.com'); + expect(span.data['http.query'], 'foo=bar'); + expect(span.data['http.fragment'], 'baz'); expect(span.finished, true); }); @@ -70,7 +81,7 @@ void main() { ); await sut.transformResponse( - RequestOptions(path: 'foo'), + RequestOptions(path: requestUri.toString()), ResponseBody.fromString('', 200), ); @@ -81,8 +92,13 @@ void main() { expect(span.status, SpanStatus.ok()); expect(span.context.operation, 'serialize.http.client'); - expect(span.context.description, 'GET foo'); + expect(span.context.description, 'GET https://example.com'); + expect(span.data['method'], 'GET'); + expect(span.data['url'], 'https://example.com'); + expect(span.data['http.query'], 'foo=bar'); + expect(span.data['http.fragment'], 'baz'); }); + test('transformResponse finish span if errored request', () async { final sut = fixture.getSut(throwException: true); final tr = fixture._hub.startTransaction( @@ -93,7 +109,7 @@ void main() { try { await sut.transformResponse( - RequestOptions(path: 'foo'), + RequestOptions(path: requestUri.toString()), ResponseBody.fromString('', 200), ); } catch (_) {} @@ -105,7 +121,11 @@ void main() { expect(span.status, SpanStatus.internalError()); expect(span.context.operation, 'serialize.http.client'); - expect(span.context.description, 'GET foo'); + expect(span.context.description, 'GET https://example.com'); + expect(span.data['method'], 'GET'); + expect(span.data['url'], 'https://example.com'); + expect(span.data['http.query'], 'foo=bar'); + expect(span.data['http.fragment'], 'baz'); expect(span.finished, true); }); }); @@ -121,7 +141,7 @@ class Fixture { _hub = Hub(_options); } - Transformer getSut({bool throwException = false}) { + SentryTransformer getSut({bool throwException = false}) { return SentryTransformer( transformer: MockTransformer(throwException), hub: _hub, diff --git a/dio/test/tracing_client_adapter_test.dart b/dio/test/tracing_client_adapter_test.dart index 8db5efb4cd..0e28e6c892 100644 --- a/dio/test/tracing_client_adapter_test.dart +++ b/dio/test/tracing_client_adapter_test.dart @@ -11,8 +11,8 @@ import 'mocks.dart'; import 'mocks/mock_http_client_adapter.dart'; import 'mocks/mock_transport.dart'; -final requestUri = Uri.parse('https://example.com?foo=bar'); -final requestOptions = '?foo=bar'; +final requestUri = Uri.parse('https://example.com?foo=bar#baz'); +final requestOptions = '?foo=bar#baz'; void main() { group(TracingClientAdapter, () { @@ -41,7 +41,11 @@ void main() { expect(span.status, SpanStatus.ok()); expect(span.context.operation, 'http.client'); - expect(span.context.description, 'GET https://example.com?foo=bar'); + expect(span.context.description, 'GET https://example.com'); + expect(span.data['method'], 'GET'); + expect(span.data['url'], 'https://example.com'); + expect(span.data['http.query'], 'foo=bar'); + expect(span.data['http.fragment'], 'baz'); }); test('finish span if errored request', () async { diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 508091c9ea..c30ca1f5ad 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -1,3 +1,4 @@ +// ignore: invalid_export_of_internal_element /// A Flutter client for Sentry.io crash reporting. export 'package:sentry/sentry.dart';