From a8cee186430a37b1cdcf746fb7d8826e48f9cc3c Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 22 Apr 2025 17:39:10 +0200 Subject: [PATCH 01/11] add sentry_supabase package --- supabase/.gitignore | 14 ++++++ supabase/CHANGELOG.md | 1 + supabase/LICENSE | 21 +++++++++ supabase/README.md | 48 ++++++++++++++++++++ supabase/analysis_options.yaml | 30 ++++++++++++ supabase/dartdoc_options.yaml | 1 + supabase/example/supabase_example.dart | 6 +++ supabase/lib/sentry_supabase.dart | 8 ++++ supabase/lib/src/sentry_supabase_client.dart | 17 +++++++ supabase/pubspec.yaml | 17 +++++++ supabase/pubspec_overrides.yaml | 3 ++ supabase/test/supabase_test.dart | 16 +++++++ 12 files changed, 182 insertions(+) create mode 100644 supabase/.gitignore create mode 120000 supabase/CHANGELOG.md create mode 100644 supabase/LICENSE create mode 100644 supabase/README.md create mode 100644 supabase/analysis_options.yaml create mode 120000 supabase/dartdoc_options.yaml create mode 100644 supabase/example/supabase_example.dart create mode 100644 supabase/lib/sentry_supabase.dart create mode 100644 supabase/lib/src/sentry_supabase_client.dart create mode 100644 supabase/pubspec.yaml create mode 100644 supabase/pubspec_overrides.yaml create mode 100644 supabase/test/supabase_test.dart diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000000..ba521d5a39 --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,14 @@ +# Omit committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ diff --git a/supabase/CHANGELOG.md b/supabase/CHANGELOG.md new file mode 120000 index 0000000000..04c99a55ca --- /dev/null +++ b/supabase/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/supabase/LICENSE b/supabase/LICENSE new file mode 100644 index 0000000000..2a6964d84d --- /dev/null +++ b/supabase/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/supabase/README.md b/supabase/README.md new file mode 100644 index 0000000000..83db2c646d --- /dev/null +++ b/supabase/README.md @@ -0,0 +1,48 @@ +

+ + + +
+

+ + +=========== + +

+ + + +
+

+ +Sentry integration for `supabase` package +=========== + +| package | build | pub | likes | popularity | pub points | +|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| ------- | +| sentry_supabase | [![build](https://github.com/getsentry/sentry-dart/actions/workflows/supabase.yml/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-supabase) | [![pub package](https://img.shields.io/pub/v/sentry_supabase.svg)](https://pub.dev/packages/sentry_supabase) | [![likes](https://img.shields.io/pub/likes/sentry_supabase)](https://pub.dev/packages/sentry_supabase/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_supabase)](https://pub.dev/packages/sentry_supabase/score) | [![pub points](https://img.shields.io/pub/points/sentry_supabase)](https://pub.dev/packages/sentry_supabase/score) + +Integration for [`supabase`](https://pub.dev/packages/supabase) package. + +#### Usage + +- Sign up for a Sentry.io account and get a DSN at https://sentry.io. + +- Follow the installing instructions on [pub.dev](https://pub.dev/packages/sentry/install). + +- Initialize the Sentry SDK using the DSN issued by Sentry.io. + +- Call... + +```dart + +``` + +#### Resources + +* [![Flutter docs](https://img.shields.io/badge/documentation-sentry.io-green.svg?label=flutter%20docs)](https://docs.sentry.io/platforms/flutter/) +* [![Dart docs](https://img.shields.io/badge/documentation-sentry.io-green.svg?label=dart%20docs)](https://docs.sentry.io/platforms/dart/) +* [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-dart.svg)](https://github.com/getsentry/sentry-dart/discussions) +* [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K) +* [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry) +* [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) diff --git a/supabase/analysis_options.yaml b/supabase/analysis_options.yaml new file mode 100644 index 0000000000..dee8927aaf --- /dev/null +++ b/supabase/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/supabase/dartdoc_options.yaml b/supabase/dartdoc_options.yaml new file mode 120000 index 0000000000..7cbb8c0d74 --- /dev/null +++ b/supabase/dartdoc_options.yaml @@ -0,0 +1 @@ +../dart/dartdoc_options.yaml \ No newline at end of file diff --git a/supabase/example/supabase_example.dart b/supabase/example/supabase_example.dart new file mode 100644 index 0000000000..2ac3227c0d --- /dev/null +++ b/supabase/example/supabase_example.dart @@ -0,0 +1,6 @@ +import 'package:supabase/sentry_supabase.dart'; + +void main() { + final client = SentrySupabaseClient(); + // TODO: Add supabase instumentation sample +} diff --git a/supabase/lib/sentry_supabase.dart b/supabase/lib/sentry_supabase.dart new file mode 100644 index 0000000000..25efb745a7 --- /dev/null +++ b/supabase/lib/sentry_supabase.dart @@ -0,0 +1,8 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/sentry_supabase.dart'; + +// TODO: Export any libraries intended for clients of this package. diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart new file mode 100644 index 0000000000..a15dedd0d7 --- /dev/null +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -0,0 +1,17 @@ +import 'package:http/http.dart'; + +class SentrySupabaseClient extends BaseClient { + late final Client innerClient; + + SentrySupabaseClient({Client? client}) { + innerClient = client ?? Client(); + } + + @override + Future send(BaseRequest request) { + + // TODO: Instrument the supabase request + + return innerClient.send(request); + } +} diff --git a/supabase/pubspec.yaml b/supabase/pubspec.yaml new file mode 100644 index 0000000000..66d493f0cb --- /dev/null +++ b/supabase/pubspec.yaml @@ -0,0 +1,17 @@ +name: sentry_supabase +description: "Sentry integration to use instument Supabase." +version: 9.0.0-alpha.2 +homepage: https://docs.sentry.io/platforms/flutter/ +repository: https://github.com/getsentry/sentry-dart +issue_tracker: https://github.com/getsentry/sentry-dart/issues + +environment: + sdk: '>=3.5.0 <4.0.0' + +dependencies: + http: ^1.3.0 + +dev_dependencies: + supabase: ^2.6.0 + lints: ^5.0.0 + test: ^1.24.0 diff --git a/supabase/pubspec_overrides.yaml b/supabase/pubspec_overrides.yaml new file mode 100644 index 0000000000..16e71d16f0 --- /dev/null +++ b/supabase/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + sentry: + path: ../dart diff --git a/supabase/test/supabase_test.dart b/supabase/test/supabase_test.dart new file mode 100644 index 0000000000..1e2043c3d7 --- /dev/null +++ b/supabase/test/supabase_test.dart @@ -0,0 +1,16 @@ +import 'package:sentry_supabase/sentry_supabase.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + final client = SentrySupabaseClient(); + + setUp(() { + // Additional setup goes here. + }); + + test('Sample test', () { + expect(client, isNotNull); + }); + }); +} From 30c089b14554ccc2771854ca54ea507362481d9b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 22 Apr 2025 17:51:08 +0200 Subject: [PATCH 02/11] add to flutter/example app --- flutter/example/lib/main.dart | 17 +++++++++++++++++ flutter/example/pubspec.yaml | 2 ++ flutter/example/pubspec_overrides.yaml | 2 ++ supabase/lib/sentry_supabase.dart | 7 +------ supabase/pubspec.yaml | 1 + 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index ba3a40b9c2..01cab9f47b 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -28,6 +28,8 @@ import 'auto_close_screen.dart'; import 'drift/connection/connection.dart'; import 'drift/database.dart'; import 'isar/user.dart'; +import 'package:supabase_flutter/supabase_flutter.dart' as supabase; +import 'package:sentry_supabase/sentry_supabase.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String exampleDsn = @@ -42,6 +44,21 @@ var _isIntegrationTest = false; final GlobalKey navigatorKey = GlobalKey(); Future main() async { + final sentrySupabaseClient = SentrySupabaseClient(); + + await supabase.Supabase.initialize( + url: '', + anonKey: '', + httpClient: sentrySupabaseClient, + ); + + final supabaseClient = supabase.Supabase.instance.client; + final issues = await supabaseClient + .from('issues') + .select(); + + print(issues); + await setupSentry( () => runApp( SentryWidget( diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index ec54958579..9813b14305 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: flutter: sdk: flutter + supabase_flutter: ^2.8.4 sentry: sentry_flutter: sentry_dio: @@ -20,6 +21,7 @@ dependencies: sentry_hive: sentry_drift: sentry_isar: + sentry_supabase: universal_platform: ^1.0.0 feedback: ^2.0.0 provider: ^6.0.0 diff --git a/flutter/example/pubspec_overrides.yaml b/flutter/example/pubspec_overrides.yaml index 8f3cdc6729..90368dc0e6 100644 --- a/flutter/example/pubspec_overrides.yaml +++ b/flutter/example/pubspec_overrides.yaml @@ -27,3 +27,5 @@ dependency_overrides: isar_generator: version: ^3.1.0 hosted: https://pub.isar-community.dev/ + sentry_supabase: + path: ../../supabase diff --git a/supabase/lib/sentry_supabase.dart b/supabase/lib/sentry_supabase.dart index 25efb745a7..eb796b2e8e 100644 --- a/supabase/lib/sentry_supabase.dart +++ b/supabase/lib/sentry_supabase.dart @@ -1,8 +1,3 @@ -/// Support for doing something awesome. -/// -/// More dartdocs go here. library; -export 'src/sentry_supabase.dart'; - -// TODO: Export any libraries intended for clients of this package. +export 'src/sentry_supabase_client.dart'; diff --git a/supabase/pubspec.yaml b/supabase/pubspec.yaml index 66d493f0cb..c1bf951208 100644 --- a/supabase/pubspec.yaml +++ b/supabase/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: http: ^1.3.0 + sentry: 9.0.0-alpha.2 dev_dependencies: supabase: ^2.6.0 From 34b09231ea4d798e691335dfc2a9355d731c0e8a Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 14:06:39 +0200 Subject: [PATCH 03/11] instument with breadcrumbs for basic operations --- flutter/example/pubspec.yaml | 2 +- supabase/lib/src/operation.dart | 10 + supabase/lib/src/sentry_supabase_client.dart | 66 ++++++- .../test/sentry_supabase_client_test.dart | 181 ++++++++++++++++++ supabase/test/supabase_test.dart | 16 -- 5 files changed, 250 insertions(+), 25 deletions(-) create mode 100644 supabase/lib/src/operation.dart create mode 100644 supabase/test/sentry_supabase_client_test.dart delete mode 100644 supabase/test/supabase_test.dart diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index f3c793def7..7467a4d782 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - supabase_flutter: ^2.8.4 + supabase_flutter: ^2.9.0 sentry: sentry_flutter: sentry_dio: diff --git a/supabase/lib/src/operation.dart b/supabase/lib/src/operation.dart new file mode 100644 index 0000000000..40d0bd6957 --- /dev/null +++ b/supabase/lib/src/operation.dart @@ -0,0 +1,10 @@ +enum Operation { + select('select'), + insert('insert'), + upsert('upsert'), + update('update'), + delete('delete'); + + final String value; + const Operation(this.value); +} diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index a15dedd0d7..323c0fa42e 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -1,17 +1,67 @@ import 'package:http/http.dart'; +import 'operation.dart'; +import 'package:sentry/sentry.dart'; class SentrySupabaseClient extends BaseClient { - late final Client innerClient; - - SentrySupabaseClient({Client? client}) { - innerClient = client ?? Client(); - } - + final Client _client; + final Hub _hub; + + SentrySupabaseClient({Client? client, Hub? hub}) : + _client = client ?? Client(), + _hub = hub ?? HubAdapter(); + @override Future send(BaseRequest request) { + final url = request.url; + final method = request.method; + final headers = request.headers; + + final table = url.pathSegments.last; + final description = 'from($table)'; + final operation = extractOperation(method, headers); - // TODO: Instrument the supabase request + if (operation != null) { + _addBreadcrumb(description, operation: operation); + } + + return _client.send(request); + } + + void _addBreadcrumb(String description, {required Operation operation}) { + final breadcrumb = Breadcrumb( + message: description, + category: 'db.${operation.value}', + type: 'supabase', + ); + _hub.addBreadcrumb(breadcrumb); + } - return innerClient.send(request); + Operation? extractOperation(String method, Map headers) { + switch (method) { + case "GET": + { + return Operation.select; + } + case "POST": + { + if (headers["Prefer"]?.contains("resolution=") ?? false) { + return Operation.upsert; + } else { + return Operation.insert; + } + } + case "PATCH": + { + return Operation.update; + } + case "DELETE": + { + return Operation.delete; + } + default: + { + return null; + } + } } } diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart new file mode 100644 index 0000000000..265862b55d --- /dev/null +++ b/supabase/test/sentry_supabase_client_test.dart @@ -0,0 +1,181 @@ +import 'package:sentry_supabase/sentry_supabase.dart'; +import 'package:test/test.dart'; +import 'package:sentry/sentry.dart'; +import 'package:http/http.dart'; +import 'dart:convert'; + +import 'dart:async'; + +import 'package:supabase/supabase.dart'; + +void main() { + const supabaseUrl = 'YOUR_SUPABASE_URL'; + const supabaseKey = 'YOUR_ANON_KEY'; + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('Client', () { + test('calls send on innser client', () async { + final sentrySupabaseClient = fixture.getSut(); + + final request = Request('GET', Uri.parse('https://example.com/123')); + + await sentrySupabaseClient.send(request); + + expect(fixture.mockClient.sendCalls.length, 1); + expect(fixture.mockClient.sendCalls.first, request); + }); + }); + + group('Breadcrumb', () { + test('select adds a breadcrumb', () async { + final sentrySupabaseClient = fixture.getSut(); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + httpClient: sentrySupabaseClient, + ); + + try { + await supabase.from('countries').select(); + } catch (e) { + print(e); + } + + expect(fixture.mockHub.addBreadcrumbCalls.length, 1); + final breadcrumb = fixture.mockHub.addBreadcrumbCalls.first.$1; + expect(breadcrumb.message, 'from(countries)'); + expect(breadcrumb.category, 'db.select'); + expect(breadcrumb.type, 'supabase'); + }); + + test('insert adds a breadcrumb', () async { + final sentrySupabaseClient = fixture.getSut(); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + httpClient: sentrySupabaseClient, + ); + + try { + await supabase.from('countries').insert({}); + } catch (e) { + print(e); + } + + expect(fixture.mockHub.addBreadcrumbCalls.length, 1); + final breadcrumb = fixture.mockHub.addBreadcrumbCalls.first.$1; + expect(breadcrumb.message, 'from(countries)'); + expect(breadcrumb.category, 'db.insert'); + expect(breadcrumb.type, 'supabase'); + }); + + test('upsert adds a breadcrumb', () async { + final sentrySupabaseClient = fixture.getSut(); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + httpClient: sentrySupabaseClient, + ); + + try { + await supabase.from('countries').upsert({}); + } catch (e) { + print(e); + } + + expect(fixture.mockHub.addBreadcrumbCalls.length, 1); + final breadcrumb = fixture.mockHub.addBreadcrumbCalls.first.$1; + expect(breadcrumb.message, 'from(countries)'); + expect(breadcrumb.category, 'db.upsert'); + expect(breadcrumb.type, 'supabase'); + }); + + test('update adds a breadcrumb', () async { + final sentrySupabaseClient = fixture.getSut(); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + httpClient: sentrySupabaseClient, + ); + + try { + await supabase.from('countries').update({}); + } catch (e) { + print(e); + } + + expect(fixture.mockHub.addBreadcrumbCalls.length, 1); + final breadcrumb = fixture.mockHub.addBreadcrumbCalls.first.$1; + expect(breadcrumb.message, 'from(countries)'); + expect(breadcrumb.category, 'db.update'); + expect(breadcrumb.type, 'supabase'); + }); + + test('delete adds a breadcrumb', () async { + final sentrySupabaseClient = fixture.getSut(); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + httpClient: sentrySupabaseClient, + ); + + try { + await supabase.from('countries').delete(); + } catch (e) { + print(e); + } + + expect(fixture.mockHub.addBreadcrumbCalls.length, 1); + final breadcrumb = fixture.mockHub.addBreadcrumbCalls.first.$1; + expect(breadcrumb.message, 'from(countries)'); + expect(breadcrumb.category, 'db.delete'); + expect(breadcrumb.type, 'supabase'); + }); + }); +} + +class Fixture { + final options = SentryOptions( + dsn: 'https://example.com/123', + ); + final mockClient = MockClient(); + final mockHub = MockHub(); + + SentrySupabaseClient getSut() { + return SentrySupabaseClient( + client: mockClient, + hub: mockHub, + ); + } +} + +class MockClient extends BaseClient { + final sendCalls = []; + final closeCalls = []; + + @override + Future send(BaseRequest request) async { + sendCalls.add(request); + return StreamedResponse(Stream.value(utf8.encode('{}')), 200); + } +} + +class MockHub implements Hub { + final addBreadcrumbCalls = <(Breadcrumb, Hint?)>[]; + + @override + Future addBreadcrumb(Breadcrumb crumb, {Hint? hint}) async { + addBreadcrumbCalls.add((crumb, hint)); + } + + // No such method + @override + void noSuchMethod(Invocation invocation) { + 'Method ${invocation.memberName} was called ' + 'with arguments ${invocation.positionalArguments}'; + } +} diff --git a/supabase/test/supabase_test.dart b/supabase/test/supabase_test.dart deleted file mode 100644 index 1e2043c3d7..0000000000 --- a/supabase/test/supabase_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:sentry_supabase/sentry_supabase.dart'; -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - final client = SentrySupabaseClient(); - - setUp(() { - // Additional setup goes here. - }); - - test('Sample test', () { - expect(client, isNotNull); - }); - }); -} From b10426552574eb28ad96137142ff1b3ac9e3cf94 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 14:09:58 +0200 Subject: [PATCH 04/11] fix typo --- supabase/test/sentry_supabase_client_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart index 265862b55d..32124ce90a 100644 --- a/supabase/test/sentry_supabase_client_test.dart +++ b/supabase/test/sentry_supabase_client_test.dart @@ -18,7 +18,7 @@ void main() { }); group('Client', () { - test('calls send on innser client', () async { + test('calls send on inner client', () async { final sentrySupabaseClient = fixture.getSut(); final request = Request('GET', Uri.parse('https://example.com/123')); From 4459e95ee35a2c395dcf9b8debc26198de257f8d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 14:15:23 +0200 Subject: [PATCH 05/11] only add breadcrumb when enabled --- supabase/lib/src/sentry_supabase_client.dart | 8 ++-- .../test/sentry_supabase_client_test.dart | 45 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index 323c0fa42e..476459e2b6 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -3,10 +3,12 @@ import 'operation.dart'; import 'package:sentry/sentry.dart'; class SentrySupabaseClient extends BaseClient { + final bool _breadcrumbs; final Client _client; final Hub _hub; - SentrySupabaseClient({Client? client, Hub? hub}) : + SentrySupabaseClient({required bool breadcrumbs, Client? client, Hub? hub}) : + _breadcrumbs = breadcrumbs, _client = client ?? Client(), _hub = hub ?? HubAdapter(); @@ -19,8 +21,8 @@ class SentrySupabaseClient extends BaseClient { final table = url.pathSegments.last; final description = 'from($table)'; final operation = extractOperation(method, headers); - - if (operation != null) { + + if (operation != null && _breadcrumbs) { _addBreadcrumb(description, operation: operation); } diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart index 32124ce90a..8f1dcf2fe9 100644 --- a/supabase/test/sentry_supabase_client_test.dart +++ b/supabase/test/sentry_supabase_client_test.dart @@ -135,6 +135,48 @@ void main() { expect(breadcrumb.category, 'db.delete'); expect(breadcrumb.type, 'supabase'); }); + + test('does not add breadcrumb when breadcrumbs are disabled', () async { + final sentrySupabaseClient = fixture.getSut(breadcrumbs: false); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + httpClient: sentrySupabaseClient, + ); + + try { + await supabase.from('countries').select(); + } catch (e) { + print(e); + } + + try { + await supabase.from('countries').insert({}); + } catch (e) { + print(e); + } + + try { + await supabase.from('countries').upsert({}); + } catch (e) { + print(e); + } + + try { + await supabase.from('countries').update({}); + } catch (e) { + print(e); + } + + try { + await supabase.from('countries').delete(); + } catch (e) { + print(e); + } + + expect(fixture.mockHub.addBreadcrumbCalls.length, 0); + }); + }); } @@ -145,8 +187,9 @@ class Fixture { final mockClient = MockClient(); final mockHub = MockHub(); - SentrySupabaseClient getSut() { + SentrySupabaseClient getSut({bool breadcrumbs = true}) { return SentrySupabaseClient( + breadcrumbs: breadcrumbs, client: mockClient, hub: mockHub, ); From cac23c48f2506932f37c7351aa3c0078cf1aa9af Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 14:34:51 +0200 Subject: [PATCH 06/11] add query to breadcrumb --- supabase/lib/src/sentry_supabase_client.dart | 98 +++++++++++++++++-- .../test/sentry_supabase_client_test.dart | 14 ++- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index 476459e2b6..e3de9cbecb 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -7,6 +7,36 @@ class SentrySupabaseClient extends BaseClient { final Client _client; final Hub _hub; + static const Map filterMappings = { + "eq": "eq", + "neq": "neq", + "gt": "gt", + "gte": "gte", + "lt": "lt", + "lte": "lte", + "like": "like", + "like(all)": "likeAllOf", + "like(any)": "likeAnyOf", + "ilike": "ilike", + "ilike(all)": "ilikeAllOf", + "ilike(any)": "ilikeAnyOf", + "is": "is", + "in": "in", + "cs": "contains", + "cd": "containedBy", + "sr": "rangeGt", + "nxl": "rangeGte", + "sl": "rangeLt", + "nxr": "rangeLte", + "adj": "rangeAdjacent", + "ov": "overlaps", + "fts": "", + "plfts": "plain", + "phfts": "phrase", + "wfts": "websearch", + "not": "not", + }; + SentrySupabaseClient({required bool breadcrumbs, Client? client, Hub? hub}) : _breadcrumbs = breadcrumbs, _client = client ?? Client(), @@ -14,31 +44,49 @@ class SentrySupabaseClient extends BaseClient { @override Future send(BaseRequest request) { - final url = request.url; final method = request.method; final headers = request.headers; + final operation = _extractOperation(method, headers); + + if (operation != null) { + _instrument(request, operation); + } + + return _client.send(request); + } + void _instrument(BaseRequest request, Operation operation) { + final url = request.url; final table = url.pathSegments.last; final description = 'from($table)'; - final operation = extractOperation(method, headers); - - if (operation != null && _breadcrumbs) { - _addBreadcrumb(description, operation: operation); + + final query = []; + for (final entry in request.url.queryParameters.entries) { + query.add(_translateFiltersIntoMethods(entry.key, entry.value)); } - return _client.send(request); + if (_breadcrumbs) { + _addBreadcrumb(description, operation, query); + } } - void _addBreadcrumb(String description, {required Operation operation}) { + void _addBreadcrumb(String description, Operation operation, List query) { final breadcrumb = Breadcrumb( message: description, category: 'db.${operation.value}', type: 'supabase', ); + + if (query.isNotEmpty) { + breadcrumb.data = { + 'query': query, + }; + } + _hub.addBreadcrumb(breadcrumb); } - Operation? extractOperation(String method, Map headers) { + Operation? _extractOperation(String method, Map headers) { switch (method) { case "GET": { @@ -66,4 +114,38 @@ class SentrySupabaseClient extends BaseClient { } } } + + String _translateFiltersIntoMethods(String key, String query) { + if (query.isEmpty || query == "*") { + return "select(*)"; + } + + if (key == "select") { + return "select($query)"; + } + + if (key == "or" || key.endsWith(".or")) { + return "$key$query"; + } + + final parts = query.split("."); + final filter = parts[0]; + final value = parts.sublist(1).join("."); + + String method; + // Handle optional `configPart` of the filter + if (filter.startsWith("fts")) { + method = "textSearch"; + } else if (filter.startsWith("plfts")) { + method = "textSearch[plain]"; + } else if (filter.startsWith("phfts")) { + method = "textSearch[phrase]"; + } else if (filter.startsWith("wfts")) { + method = "textSearch[websearch]"; + } else { + method = filterMappings[filter] ?? "filter"; + } + + return "$method($key, $value)"; + } } diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart index 8f1dcf2fe9..f33a1e1a0e 100644 --- a/supabase/test/sentry_supabase_client_test.dart +++ b/supabase/test/sentry_supabase_client_test.dart @@ -40,7 +40,7 @@ void main() { ); try { - await supabase.from('countries').select(); + await supabase.from('countries').select().eq('id', 42); } catch (e) { print(e); } @@ -50,6 +50,7 @@ void main() { expect(breadcrumb.message, 'from(countries)'); expect(breadcrumb.category, 'db.select'); expect(breadcrumb.type, 'supabase'); + expect(breadcrumb.data?['query'], ['select(*)', 'eq(id, 42)']); }); test('insert adds a breadcrumb', () async { @@ -61,7 +62,7 @@ void main() { ); try { - await supabase.from('countries').insert({}); + await supabase.from('countries').insert({ 'id': 42 }); } catch (e) { print(e); } @@ -82,7 +83,7 @@ void main() { ); try { - await supabase.from('countries').upsert({}); + await supabase.from('countries').upsert({ 'id': 42 }).select(); } catch (e) { print(e); } @@ -92,6 +93,7 @@ void main() { expect(breadcrumb.message, 'from(countries)'); expect(breadcrumb.category, 'db.upsert'); expect(breadcrumb.type, 'supabase'); + expect(breadcrumb.data?['query'], ['select(*)']); }); test('update adds a breadcrumb', () async { @@ -103,7 +105,7 @@ void main() { ); try { - await supabase.from('countries').update({}); + await supabase.from('countries').update({ 'id': 1337 }).eq('id', 42); } catch (e) { print(e); } @@ -113,6 +115,7 @@ void main() { expect(breadcrumb.message, 'from(countries)'); expect(breadcrumb.category, 'db.update'); expect(breadcrumb.type, 'supabase'); + expect(breadcrumb.data?['query'], ['eq(id, 42)']); }); test('delete adds a breadcrumb', () async { @@ -124,7 +127,7 @@ void main() { ); try { - await supabase.from('countries').delete(); + await supabase.from('countries').delete().eq('id', 42); } catch (e) { print(e); } @@ -134,6 +137,7 @@ void main() { expect(breadcrumb.message, 'from(countries)'); expect(breadcrumb.category, 'db.delete'); expect(breadcrumb.type, 'supabase'); + expect(breadcrumb.data?['query'], ['eq(id, 42)']); }); test('does not add breadcrumb when breadcrumbs are disabled', () async { From 7bc29d3ef7b803d4a9c5ee6b10af7fdaf28334d7 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 14:37:19 +0200 Subject: [PATCH 07/11] format --- supabase/lib/src/sentry_supabase_client.dart | 25 ++++++++++++------- .../test/sentry_supabase_client_test.dart | 9 +++---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index e3de9cbecb..ca30991e31 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -6,7 +6,7 @@ class SentrySupabaseClient extends BaseClient { final bool _breadcrumbs; final Client _client; final Hub _hub; - + static const Map filterMappings = { "eq": "eq", "neq": "neq", @@ -37,17 +37,20 @@ class SentrySupabaseClient extends BaseClient { "not": "not", }; - SentrySupabaseClient({required bool breadcrumbs, Client? client, Hub? hub}) : - _breadcrumbs = breadcrumbs, - _client = client ?? Client(), - _hub = hub ?? HubAdapter(); - + SentrySupabaseClient({ + required bool breadcrumbs, + Client? client, + Hub? hub, + }) : _breadcrumbs = breadcrumbs, + _client = client ?? Client(), + _hub = hub ?? HubAdapter(); + @override Future send(BaseRequest request) { final method = request.method; final headers = request.headers; final operation = _extractOperation(method, headers); - + if (operation != null) { _instrument(request, operation); } @@ -70,7 +73,11 @@ class SentrySupabaseClient extends BaseClient { } } - void _addBreadcrumb(String description, Operation operation, List query) { + void _addBreadcrumb( + String description, + Operation operation, + List query, + ) { final breadcrumb = Breadcrumb( message: description, category: 'db.${operation.value}', @@ -82,7 +89,7 @@ class SentrySupabaseClient extends BaseClient { 'query': query, }; } - + _hub.addBreadcrumb(breadcrumb); } diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart index f33a1e1a0e..22d016c50d 100644 --- a/supabase/test/sentry_supabase_client_test.dart +++ b/supabase/test/sentry_supabase_client_test.dart @@ -20,7 +20,7 @@ void main() { group('Client', () { test('calls send on inner client', () async { final sentrySupabaseClient = fixture.getSut(); - + final request = Request('GET', Uri.parse('https://example.com/123')); await sentrySupabaseClient.send(request); @@ -62,7 +62,7 @@ void main() { ); try { - await supabase.from('countries').insert({ 'id': 42 }); + await supabase.from('countries').insert({'id': 42}); } catch (e) { print(e); } @@ -83,7 +83,7 @@ void main() { ); try { - await supabase.from('countries').upsert({ 'id': 42 }).select(); + await supabase.from('countries').upsert({'id': 42}).select(); } catch (e) { print(e); } @@ -105,7 +105,7 @@ void main() { ); try { - await supabase.from('countries').update({ 'id': 1337 }).eq('id', 42); + await supabase.from('countries').update({'id': 1337}).eq('id', 42); } catch (e) { print(e); } @@ -180,7 +180,6 @@ void main() { expect(fixture.mockHub.addBreadcrumbCalls.length, 0); }); - }); } From d4b10363266060310e7d6df6be04fea7d562c6a0 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 14:50:27 +0200 Subject: [PATCH 08/11] add body to breadcrumb --- supabase/lib/src/sentry_supabase_client.dart | 20 +++++++++++++++---- .../test/sentry_supabase_client_test.dart | 3 +++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index ca30991e31..0d3563038a 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -1,6 +1,7 @@ import 'package:http/http.dart'; import 'operation.dart'; import 'package:sentry/sentry.dart'; +import 'dart:convert'; class SentrySupabaseClient extends BaseClient { final bool _breadcrumbs; @@ -68,8 +69,12 @@ class SentrySupabaseClient extends BaseClient { query.add(_translateFiltersIntoMethods(entry.key, entry.value)); } + final bodyString = + request is Request && request.body.isNotEmpty ? request.body : null; + final body = bodyString != null ? jsonDecode(bodyString) : null; + if (_breadcrumbs) { - _addBreadcrumb(description, operation, query); + _addBreadcrumb(description, operation, query, body); } } @@ -77,6 +82,7 @@ class SentrySupabaseClient extends BaseClient { String description, Operation operation, List query, + Map? body, ) { final breadcrumb = Breadcrumb( message: description, @@ -84,10 +90,16 @@ class SentrySupabaseClient extends BaseClient { type: 'supabase', ); + if (query.isNotEmpty || body != null) { + breadcrumb.data = {}; + } + if (query.isNotEmpty) { - breadcrumb.data = { - 'query': query, - }; + breadcrumb.data?['query'] = query; + } + + if (body != null) { + breadcrumb.data?['body'] = body; } _hub.addBreadcrumb(breadcrumb); diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart index 22d016c50d..39d7665557 100644 --- a/supabase/test/sentry_supabase_client_test.dart +++ b/supabase/test/sentry_supabase_client_test.dart @@ -72,6 +72,7 @@ void main() { expect(breadcrumb.message, 'from(countries)'); expect(breadcrumb.category, 'db.insert'); expect(breadcrumb.type, 'supabase'); + expect(breadcrumb.data?['body'], {'id': 42}); }); test('upsert adds a breadcrumb', () async { @@ -94,6 +95,7 @@ void main() { expect(breadcrumb.category, 'db.upsert'); expect(breadcrumb.type, 'supabase'); expect(breadcrumb.data?['query'], ['select(*)']); + expect(breadcrumb.data?['body'], {'id': 42}); }); test('update adds a breadcrumb', () async { @@ -116,6 +118,7 @@ void main() { expect(breadcrumb.category, 'db.update'); expect(breadcrumb.type, 'supabase'); expect(breadcrumb.data?['query'], ['eq(id, 42)']); + expect(breadcrumb.data?['body'], {'id': 1337}); }); test('delete adds a breadcrumb', () async { From 56f7aa82c1b6e154ef492eca6677a28abc704d4d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 15:03:44 +0200 Subject: [PATCH 09/11] implement body redaction --- supabase/lib/src/sentry_supabase_client.dart | 17 ++++- .../test/sentry_supabase_client_test.dart | 66 ++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index 0d3563038a..451ffe58b9 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -3,8 +3,15 @@ import 'operation.dart'; import 'package:sentry/sentry.dart'; import 'dart:convert'; +typedef SentrySupabaseRedactRequestBody = String? Function( + String table, + String key, + String value, +); + class SentrySupabaseClient extends BaseClient { final bool _breadcrumbs; + final SentrySupabaseRedactRequestBody? _redactRequestBody; final Client _client; final Hub _hub; @@ -40,9 +47,11 @@ class SentrySupabaseClient extends BaseClient { SentrySupabaseClient({ required bool breadcrumbs, + SentrySupabaseRedactRequestBody? redactRequestBody, Client? client, Hub? hub, }) : _breadcrumbs = breadcrumbs, + _redactRequestBody = redactRequestBody, _client = client ?? Client(), _hub = hub ?? HubAdapter(); @@ -71,7 +80,13 @@ class SentrySupabaseClient extends BaseClient { final bodyString = request is Request && request.body.isNotEmpty ? request.body : null; - final body = bodyString != null ? jsonDecode(bodyString) : null; + var body = bodyString != null ? jsonDecode(bodyString) : null; + + if (_redactRequestBody != null) { + for (final entry in body?.entries ?? []) { + body[entry.key] = _redactRequestBody(table, entry.key, entry.value); + } + } if (_breadcrumbs) { _addBreadcrumb(description, operation, query, body); diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart index 39d7665557..9131919166 100644 --- a/supabase/test/sentry_supabase_client_test.dart +++ b/supabase/test/sentry_supabase_client_test.dart @@ -183,6 +183,67 @@ void main() { expect(fixture.mockHub.addBreadcrumbCalls.length, 0); }); + + test('redact request body', () async { + final sentrySupabaseClient = fixture.getSut( + redactRequestBody: (table, key, value) { + switch (key) { + case "password": + return ""; + case "token": + return ""; + case "secret": + return ""; + case "null-me": + return null; + default: + { + return value; + } + } + }, + ); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + httpClient: sentrySupabaseClient, + ); + + try { + await supabase.from("mock-table").insert( + {'user': 'picklerick', 'password': 'whoops', 'null-me': 'foo'}); + } catch (e) { + print(e); + } + + try { + await supabase + .from("mock-table") + .upsert({'user': 'picklerick', 'token': 'whoops'}); + } catch (e) { + print(e); + } + + try { + await supabase + .from("mock-table") + .update({'user': 'picklerick', 'secret': 'whoops'}).eq("id", 42); + } catch (e) { + print(e); + } + + expect(fixture.mockHub.addBreadcrumbCalls.length, 3); + final inserted = fixture.mockHub.addBreadcrumbCalls[0].$1; + expect(inserted.data?['body'], + {'user': 'picklerick', 'password': '', 'null-me': null}); + + final upserted = fixture.mockHub.addBreadcrumbCalls[1].$1; + expect(upserted.data?['body'], {'user': 'picklerick', 'token': ''}); + + final updated = fixture.mockHub.addBreadcrumbCalls[2].$1; + expect( + updated.data?['body'], {'user': 'picklerick', 'secret': ''}); + }); }); } @@ -193,11 +254,14 @@ class Fixture { final mockClient = MockClient(); final mockHub = MockHub(); - SentrySupabaseClient getSut({bool breadcrumbs = true}) { + SentrySupabaseClient getSut( + {bool breadcrumbs = true, + SentrySupabaseRedactRequestBody? redactRequestBody}) { return SentrySupabaseClient( breadcrumbs: breadcrumbs, client: mockClient, hub: mockHub, + redactRequestBody: redactRequestBody, ); } } From f1df16d4bf598ef0e00930ce02e7b5ee44275383 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 15:08:47 +0200 Subject: [PATCH 10/11] format --- supabase/lib/src/sentry_supabase_client.dart | 26 ++++++------------- .../test/sentry_supabase_client_test.dart | 4 +-- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index 451ffe58b9..6296e9546b 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -123,29 +123,19 @@ class SentrySupabaseClient extends BaseClient { Operation? _extractOperation(String method, Map headers) { switch (method) { case "GET": - { - return Operation.select; - } + return Operation.select; case "POST": - { - if (headers["Prefer"]?.contains("resolution=") ?? false) { - return Operation.upsert; - } else { - return Operation.insert; - } + if (headers["Prefer"]?.contains("resolution=") ?? false) { + return Operation.upsert; + } else { + return Operation.insert; } case "PATCH": - { - return Operation.update; - } + return Operation.update; case "DELETE": - { - return Operation.delete; - } + return Operation.delete; default: - { - return null; - } + return null; } } diff --git a/supabase/test/sentry_supabase_client_test.dart b/supabase/test/sentry_supabase_client_test.dart index 9131919166..420912bf29 100644 --- a/supabase/test/sentry_supabase_client_test.dart +++ b/supabase/test/sentry_supabase_client_test.dart @@ -197,9 +197,7 @@ void main() { case "null-me": return null; default: - { - return value; - } + return value; } }, ); From a7b8b280479931c65c061c5710eb5be400cacbc0 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 6 May 2025 15:13:23 +0200 Subject: [PATCH 11/11] refactor --- supabase/lib/src/sentry_supabase_client.dart | 26 +++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/supabase/lib/src/sentry_supabase_client.dart b/supabase/lib/src/sentry_supabase_client.dart index 6296e9546b..69fffcc6c8 100644 --- a/supabase/lib/src/sentry_supabase_client.dart +++ b/supabase/lib/src/sentry_supabase_client.dart @@ -72,25 +72,33 @@ class SentrySupabaseClient extends BaseClient { final url = request.url; final table = url.pathSegments.last; final description = 'from($table)'; + final query = _readQuery(request); + final body = _readBody(table, request); - final query = []; - for (final entry in request.url.queryParameters.entries) { - query.add(_translateFiltersIntoMethods(entry.key, entry.value)); + if (_breadcrumbs) { + _addBreadcrumb(description, operation, query, body); } + } + + List _readQuery(BaseRequest request) { + return request.url.queryParameters.entries + .map( + (entry) => _translateFiltersIntoMethods(entry.key, entry.value), + ) + .toList(); + } + Map? _readBody(String table, BaseRequest request) { final bodyString = request is Request && request.body.isNotEmpty ? request.body : null; var body = bodyString != null ? jsonDecode(bodyString) : null; - if (_redactRequestBody != null) { - for (final entry in body?.entries ?? []) { + if (body != null && _redactRequestBody != null) { + for (final entry in body.entries) { body[entry.key] = _redactRequestBody(table, entry.key, entry.value); } } - - if (_breadcrumbs) { - _addBreadcrumb(description, operation, query, body); - } + return body; } void _addBreadcrumb(