Skip to content

Supabase Support #2913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -42,6 +44,21 @@ var _isIntegrationTest = false;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

Future<void> main() async {
final sentrySupabaseClient = SentrySupabaseClient();

await supabase.Supabase.initialize(
url: '<YOUR_SUPABASE_URL>',
anonKey: '<YOUR_SUPABASE_ANON_KEY>',
httpClient: sentrySupabaseClient,
);

final supabaseClient = supabase.Supabase.instance.client;
final issues = await supabaseClient
.from('issues')
.select();

print(issues);

await setupSentry(
() => runApp(
SentryWidget(
Expand Down
2 changes: 2 additions & 0 deletions flutter/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ environment:
dependencies:
flutter:
sdk: flutter
supabase_flutter: ^2.9.0
sentry:
sentry_flutter:
sentry_dio:
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions flutter/example/pubspec_overrides.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ dependency_overrides:
isar_generator:
version: ^3.1.0
hosted: https://pub.isar-community.dev/
sentry_supabase:
path: ../../supabase
14 changes: 14 additions & 0 deletions supabase/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
1 change: 1 addition & 0 deletions supabase/CHANGELOG.md
21 changes: 21 additions & 0 deletions supabase/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
48 changes: 48 additions & 0 deletions supabase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<p align="center">
<a href="https://sentry.io" target="_blank" align="center">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
</a>
<br />
</p>


===========

<p align="center">
<a href="https://sentry.io" target="_blank" align="center">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
</a>
<br />
</p>

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)
30 changes: 30 additions & 0 deletions supabase/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions supabase/dartdoc_options.yaml
6 changes: 6 additions & 0 deletions supabase/example/supabase_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:supabase/sentry_supabase.dart';

void main() {
final client = SentrySupabaseClient();
// TODO: Add supabase instumentation sample
}
3 changes: 3 additions & 0 deletions supabase/lib/sentry_supabase.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
library;

export 'src/sentry_supabase_client.dart';
10 changes: 10 additions & 0 deletions supabase/lib/src/operation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
enum Operation {
select('select'),
insert('insert'),
upsert('upsert'),
update('update'),
delete('delete');

final String value;
const Operation(this.value);
}
183 changes: 183 additions & 0 deletions supabase/lib/src/sentry_supabase_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import 'package:http/http.dart';
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;

static const Map<String, String> 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,
SentrySupabaseRedactRequestBody? redactRequestBody,
Client? client,
Hub? hub,
}) : _breadcrumbs = breadcrumbs,
_redactRequestBody = redactRequestBody,
_client = client ?? Client(),
_hub = hub ?? HubAdapter();

@override
Future<StreamedResponse> send(BaseRequest request) {
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 query = _readQuery(request);
final body = _readBody(table, request);

if (_breadcrumbs) {
_addBreadcrumb(description, operation, query, body);
}
}

List<String> _readQuery(BaseRequest request) {
return request.url.queryParameters.entries
.map(
(entry) => _translateFiltersIntoMethods(entry.key, entry.value),
)
.toList();
}

Map<String, dynamic>? _readBody(String table, BaseRequest request) {
final bodyString =
request is Request && request.body.isNotEmpty ? request.body : null;
var body = bodyString != null ? jsonDecode(bodyString) : null;

if (body != null && _redactRequestBody != null) {
for (final entry in body.entries) {
body[entry.key] = _redactRequestBody(table, entry.key, entry.value);
}
}
return body;
}

void _addBreadcrumb(
String description,
Operation operation,
List<String> query,
Map<String, dynamic>? body,
) {
final breadcrumb = Breadcrumb(
message: description,
category: 'db.${operation.value}',
type: 'supabase',
);

if (query.isNotEmpty || body != null) {
breadcrumb.data = {};
}

if (query.isNotEmpty) {
breadcrumb.data?['query'] = query;
}

if (body != null) {
breadcrumb.data?['body'] = body;
}

_hub.addBreadcrumb(breadcrumb);
}

Operation? _extractOperation(String method, Map<String, String> 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;
}
}

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)";
}
}
Loading
Loading