diff --git a/CHANGELOG.md b/CHANGELOG.md index f418807..ba8b03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.1.0 + +- Update angular 5 +- Update sentry package +- ***Warning*** refactoring ***Breaking Change***: + + use OpaqueToken to pass release version, environment, dsn and logger + sentryEnvironmentToken,sentryReleaseVersionToken, sentryLoggerToken, sentryDsnToken + + no more tags and extra field, override `transformEvent` function instead + + rename `onCatch` to `capture` + ## 0.0.4 - update sentry package to 1.0.0 diff --git a/README.md b/README.md index d0fc110..fe68891 100644 --- a/README.md +++ b/README.md @@ -10,43 +10,71 @@ Helper to implements sentry with Angular. import "package:angular/angular.dart"; import "package:angular_sentry/angular_sentry.dart"; +// ignore: uri_has_not_been_generated +import 'main.template.dart' as ng; + +const sentryModule = Module(provide: [ + ValueProvider.forToken(sentryLoggerToken, "MY_SENTRY_DSN"), + ValueProvider.forToken(sentryEnvironmentToken, "production"), + ValueProvider.forToken(sentryReleaseVersionToken, "1.0.0"), + ClassProvider( + ExceptionHandler, + useClass: AngularSentry, + ), +]); + +@GenerateInjector( + [sentryModule], +) +const scannerApp = ng.scannerApp$Injector; + main() { - bootstrap(MyApp, [ - provide(SENTRY_DSN, useValue: "MY_SENTRY_DSN"), - provide(ExceptionHandler, useClass: AngularSentry) - ]); + runApp(appComponentNgFactory, createInjector: scannerApp); } - ``` ### Advanced +Implement your own class using AngularSentry + ```dart +const sentryModule = Module(provide: [ + ... + ClassProvider( + ExceptionHandler, + useClass: AppSentry, + ), +]); + main() { - bootstrap(MyApp, [ - AppSentry - ]); + runApp(appComponentNgFactory, createInjector: scannerApp); } -@Injectable() class AppSentry extends AngularSentry { - AppSentry(Injector injector) - : super(injector, "MY_SENTRY_DSN"); - - SentryUser get user => new SentryUser(); - - String get environment => "production"; - - String get release => "1.0.0"; - - Map get extra => {"location_url": window.location.href}; + AppSentry(Injector injector, NgZone zone) + : super( + injector, + zone, + dsn: "MY_SENTRY_DSN", + environment: "production", + release: "1.0.0", + ); + + @override + Event transformEvent(Event e) { + return super.transformEvent(e).replace( + userContext: new User(id: '1', ipAddress: '0.0.0.0'), + extra: {"location_url": window.location.href}, + ); + } - void onCatch(dynamic exception, Trace trace, [String reason]) { + @override + void capture(exception, [trace, String reason]) { if (exception is ClientException) { - log("Network error"); + logError("Network error"); } else { - super.onCatch(exception, trace, reason); + super.capture(exception, trace, reason); } } } diff --git a/analysis_options.yaml b/analysis_options.yaml index 97f0908..8d57e21 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,15 +1,14 @@ analyzer: - strong-mode: true # exclude: # - path/to/excluded/files/** # Lint rules and documentation, see http://dart-lang.github.io/linter/lints linter: rules: - - cancel_subscriptions - - hash_and_equals - - iterable_contains_unrelated_type - - list_remove_unrelated_type - - test_types_in_equals - - unrelated_type_equality_checks - - valid_regexps + - cancel_subscriptions + - hash_and_equals + - iterable_contains_unrelated_type + - list_remove_unrelated_type + - test_types_in_equals + - unrelated_type_equality_checks + - valid_regexps diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..26ba14c --- /dev/null +++ b/build.yaml @@ -0,0 +1,8 @@ +targets: + angular_sentry: + sources: + include: ["example/**", "lib/**"] + builders: + build_web_compilers|dart_source_cleanup: + release_options: + enabled: false \ No newline at end of file diff --git a/example/app.dart b/example/app.dart new file mode 100644 index 0000000..563811c --- /dev/null +++ b/example/app.dart @@ -0,0 +1,11 @@ +import 'package:angular/angular.dart'; + +@Component( + selector: 'app', + template: '', +) +class AppComponent { + void error() { + throw Exception("Test error"); + } +} diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..df47544 --- /dev/null +++ b/example/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 0000000..96b0325 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,57 @@ +import 'dart:html' hide Event; + +import "package:angular/angular.dart"; +import "package:angular_sentry/angular_sentry.dart"; +import 'package:http/http.dart'; + +// ignore: uri_has_not_been_generated +import 'main.template.dart' as ng; + +// ignore: uri_has_not_been_generated +import 'app.template.dart' as app; + +const sentryModule = Module(provide: [ + //ValueProvider.forToken(sentryLoggerToken, "MY_SENTRY_DSN"), + ValueProvider.forToken(sentryEnvironmentToken, "production"), + ValueProvider.forToken(sentryReleaseVersionToken, "1.0.0"), + ClassProvider( + ExceptionHandler, + useClass: AngularSentry, + ), +]); + +@GenerateInjector( + [sentryModule], +) +const scannerApp = ng.scannerApp$Injector; + +main() { + runApp(app.AppComponentNgFactory, createInjector: scannerApp); +} + +class AppSentry extends AngularSentry { + AppSentry(Injector injector) + : super( + injector, + //dsn: "MY_SENTRY_DSN", + environment: "production", + release: "1.0.0", + ); + + @override + Event transformEvent(Event e) { + return super.transformEvent(e).replace( + userContext: new User(id: '1', ipAddress: '0.0.0.0'), + extra: {"location_url": window.location.href}, + ); + } + + @override + void capture(exception, [trace, String reason]) { + if (exception is ClientException) { + logError("Network error"); + } else { + super.capture(exception, trace, reason); + } + } +} diff --git a/lib/angular_sentry.dart b/lib/angular_sentry.dart index 3c46937..d238cdf 100644 --- a/lib/angular_sentry.dart +++ b/lib/angular_sentry.dart @@ -3,129 +3,162 @@ library angular_sentry; import "dart:html" as html; import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:angular/angular.dart'; import 'package:logging/logging.dart'; -import "package:sentry/sentry.dart"; +import "package:sentry/sentry_browser.dart"; +import "package:http/http.dart"; import "package:http/browser_client.dart"; -const OpaqueToken sentryDsn = const OpaqueToken('sentryDSN'); +export 'package:sentry/sentry_browser.dart'; + +typedef Event TransformEvent(Event e); + +/// Use to transform the event before sending to sentry +/// add tags or extra for example +const sentryTransformEventToken = OpaqueToken( + 'sentry.transformEvent', +); + +/// provide environment data to the sentry report +const sentryEnvironmentToken = OpaqueToken('sentry.env'); + +/// The release version of the application. +const sentryReleaseVersionToken = OpaqueToken('sentry.release'); + +/// Pass Logger to sentry +/// If no logger, it will print exception to console +const sentryLoggerToken = OpaqueToken('sentry.logger'); + +/// Provide sentry dsn +/// If no dsn provided, it will log the exception without reporting it to sentry +const sentryDsnToken = OpaqueToken('sentry.dsn'); -@Injectable() class AngularSentry implements ExceptionHandler { - SentryClient _sentry; - Logger _log; + final Logger log; + final String environment; + final String release; + final String dsn; + final Client client; + final TransformEvent eventTransformer; - Logger get log => _log; - ApplicationRef _appRef; + final _exceptionController = StreamController.broadcast(); - AngularSentry(Injector injector, @Optional() @Inject(sentryDsn) String dsn, - @Optional() BrowserClient client) { - if (dsn != null) { - _sentry = new SentryClient( - dsn: dsn, - compressPayload: false, - httpClient: client ?? new BrowserClient()); - } + Stream _onException; + SentryClientBrowser _sentry; + + ApplicationRef _appRef; - _log = new Logger("$runtimeType"); + AngularSentry( + Injector injector, { + @Optional() this.client, + @Optional() @Inject(sentryDsnToken) this.dsn, + @Optional() @Inject(sentryLoggerToken) this.log, + @Optional() @Inject(sentryEnvironmentToken) this.environment, + @Optional() @Inject(sentryReleaseVersionToken) this.release, + @Optional() @Inject(sentryTransformEventToken) this.eventTransformer, + }) { // prevent DI circular dependency - new Future.delayed(Duration.ZERO, () { + new Future.delayed(Duration.zero, () { _appRef = injector.get(ApplicationRef) as ApplicationRef; }); + + _onException = _exceptionController.stream + .map( + transformEvent, + ) + .where( + (event) => event != null, + )..listen( + _sendEvent, + onError: logError, + ); + + _initSentry(); } - void onCatch(dynamic exception, [dynamic stackTrace, String reason]) { + void _initSentry() { + if (dsn == null) return; + try { - _send(exception, stackTrace, reason); + _sentry = SentryClientBrowser( + dsn: dsn, + httpClient: client ?? BrowserClient(), + environmentAttributes: Event( + environment: environment, + release: release, + )); } catch (e, s) { logError(e, s); - // do nothing; } } - /// Log the catched error using Logging - /// Called before onCatch - void logError(dynamic exception, [dynamic stackTrace, String reason]) { - _log.severe(() => ExceptionHandler.exceptionToString( - exception, - stackTrace, - reason, - )); + void _sendEvent(Event e) { + try { + _sentry?.capture(event: e); + } catch (e, s) { + logError(e, s); + } } - @override - void call(dynamic exception, [dynamic stackTrace, String reason]) { - logError(exception, stackTrace, reason); - onCatch(exception, stackTrace, reason); - _appRef?.tick(); + /// onException stream after [transformEvent] call + Stream get onException => _onException; + + /// can be override to transform sentry report + /// adding tags or extra for example + @protected + @mustCallSuper + Event transformEvent(Event e) { + try { + if (eventTransformer == null) return e; + + return eventTransformer(e); + } catch (e, s) { + logError(e, s); + return e; + } } - /// provide environment data to the sentry report - String get environment => null; + /// Log the catched error using Logging + /// if no logger provided, print into console with window.console.error + void logError(exception, [stackTrace, String reason]) { + if (log != null) { + log.severe(reason, exception, stackTrace); + } else { + logErrorWindow(exception, stackTrace, reason); + } + } - Map get tags => { - "userAgent": html.window.navigator.userAgent, - "platform": html.window.navigator.platform - }; + /// log error using window.console.error + void logErrorWindow(exception, [stackTrace, String reason]) { + if (reason != null) html.window.console.error(reason.toString()); - /// The release version of the application. - String get release => null; + html.window.console.error(exception.toString()); - /// provide extra data to the sentry report - Map get extra => null; + if (stackTrace != null) html.window.console.error(stackTrace.toString()); + } - void _send(dynamic exception, [dynamic stackTrace, String reason]) { - final event = new Event( + @protected + @mustCallSuper + void capture(dynamic exception, [dynamic stackTrace, String reason]) => + _exceptionController.add(Event( exception: exception, - stackTrace: stackTrace is List ? stackTrace.first : stackTrace, - release: release, - environment: environment, - extra: extra, - tags: tags, - message: reason); - _sentry?.capture(event: event); + stackTrace: stackTrace, + message: reason, + )); + + @override + @protected + void call(dynamic exception, [dynamic stackTrace, String reason]) { + logError(exception, stackTrace, reason); + capture(exception, stackTrace, reason); + + // not sure about this + // the application state might not be clean + _appRef?.tick(); } -} -//Trace parseStackTrace(dynamic stackTrace) { -// if (stackTrace is StackTrace) { -// return new Trace.parse(stackTrace.toString()); -// } else if (stackTrace is String) { -// return new Trace.parse(stackTrace); -// } else if (stackTrace is Iterable && stackTrace.length == 1) { -// return new Trace.parse(stackTrace.first); -// } else if (stackTrace is Iterable) { -// final trace = stackTrace.map(parseFrame).where((f) => f != null).toList(); -// return new Trace(trace); -// } -// return new Trace.current(); -//} -// -//Frame parseFrame(f) { -// if (f is Frame) { -// return f; -// } -// Frame parsed; -// if (f is String) { -// parsed = new Frame.parseV8(f); -// if (parsed is UnparsedFrame) { -// parsed = new Frame.parseFirefox(f); -// if (parsed is UnparsedFrame) { -// parsed = new Frame.parseSafari(f); -// if (parsed is UnparsedFrame) { -// try { -// parsed = new Frame.parseFriendly(f); -// } catch (_) { -// parsed = null; -// } -// } -// } -// } -// } -// -// if (parsed is UnparsedFrame) { -// parsed = null; -// } -// -// return parsed; -//} + void dispose() { + _exceptionController.close(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 5f57582..516b964 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,17 +1,23 @@ name: angular_sentry description: Helper to implements sentry with Angular -version: 0.0.4 +version: 0.1.0 homepage: https://lefty.io author: Hadrien Lejard environment: - sdk: '>=1.20.1 <2.0.0' + sdk: '>=2.0.0-dev.65.0 <3.0.0' dependencies: - angular: ^4.0.0 - sentry: ^1.0.0 + angular: ^5.0.0-beta + sentry: + git: + url: https://github.com/lejard-h/sentry.git + ref: web logging: ^0.11.0 http: ^0.11.3 -#dev_dependencies: -# test: ^0.12.0 +dev_dependencies: + build: '>=0.11.1 <1.0.0' + build_runner: '>=0.8.0 <1.0.0' + build_test: '>=0.10.0 <1.0.0' + build_web_compilers: '>=0.4.0 <1.0.0' \ No newline at end of file