Introduce raven_dart, a Dart client for Sentry

Since I’ve been experimenting with Sentry and hacking around in Dart again lately, so what better way is there to combine these two activities than to write a Dart client for Sentry?

That said, there is already a Javascript client library for Sentry, which via the dart:js library you can probably save yourself some code by writing a wrapper around the Javascript library instead. But I chose to write the client from scratch because:

  1. it’s more fun
  2. you can take advantage of Dart’s support for static type checking
  3. you can make use of Dart’s isolates (think F# mailbox or Erlang processes, isolates is Dart’s implementation of Carl Hewitt’s actor model) to abstract over making concurrent requests and handling retries, etc.
  4. wrapping the Javascript library means hard dependency on the Javascript library being present, which means you won’t be able to use it on the server side
  5. it’s more fun!

 

Update 05/07/2014 : I had previously stated that raven-js had dependencies on jQuery, ember, etc. that was incorrect. As they were in fact raven-js plugins for those frameworks (as stated in its Plugins documentation page here) and the raven-js itself doesn’t have any dependencies.

 

And the result is a small (~600 LOC) Dart library called raven_dart, that allows you to capture generic messages or exceptions as events in Sentry. The most basic usage would look like this:

Operational params can be used to customize the behaviour of captureMessage and captureException, such as adding tags, or other metadata to go along with the events.

Following the official guidelines on writing a Sentry client, the library supports:

  • DSN configuration via constructor argument
    • null means disabled
  • Graceful failure handling
    • fallback to logging to console when Sentry is disabled
    • retry on error (except for HTTP status codes 400, 401, 403, and 503)
    • configurable max no. of retries
    • exponential delay when retrying
    • temporarily disable if Sentry is unavailable (503)
  • Support for tagging
    • common tags can be provided in the client constructor
    • additional tags are supplied for each event
    • when common and event-specific tags overlap, both are sent as part of the event
  • Non-blocking event submission
    • events are sent to available isolates in round-robin fashion, whom then process them asynchronously and concurrently
    • configurable level of concurrency per core which determines number of isolates running at the same time
  • Basic data sanitization/scrubbing
    • credit card number-like fields are scrubbed
    • Sentry key and secrets are scrubbed
    • Values that look like passwords or secrets are scrubbed

 

Please give it a try and let me know what you think, if you find any bugs or have feedbacks in general, feel free to add them to the issues page.

 

Related Links

raven_dart homepage

raven_dart on pub.dartlang.org

Libraries for C# and F# for easier integration with Sentry

Emulating F#’s discriminated unions (aka algebraic data types) in Dart

Emulating enums in Dart

Takeaways from Hewitt, Meijer and Szyperski’s talk on the Actor model

Dart – Emulating enums using Constant Constructor

Whilst Dart doesn’t have support for enum types but it turns out you can do a pretty good job of emulating it using Dart’s constant constructors (which allows you to create compile-time constants with your class, which in .Net you’re restricted to numbers, booleans, strings and null).

Using the technique from this SO answer you will start with a base class for your enum types:

From here, you can create other specialized enum types, such as:

 

Thank you, mr Sergy Akopkokhyants for this elegant little solution.

Dart – Emulating F#’s Discriminated Union (i.e. an algebraic data type)

Algebraic Data Type

An algebraic data type is a basically a composite type that is formed by combining other types, sometimes also referred to as “sums-and-products” data structures (don’t worries if this is too abstract for you, examples are coming).

A simple example would be a composite type that represents a binary tree that has:

  • Nodes – which contains a value and two children, or
  • Leaves – which contains a value but no children

Using F#’s discriminated unions we can create a generic Tree type that are composed of two subtypes, Leaf and Node, each with a different set of associated types. We can then use pattern matching to help us retrieve the information about the subtypes when we’re given an instance of Tree.

(note : in F# the types in a tuple is separated by *, so a Node contains a tuple of type T, Tree<T> and Tree<T>).

 

Let’s break down the definition for the Tree type to reveal the three building blocks for algebraic expressions that we can use to describe data structures – sums, products, and recursions (this SO answer has a nice explanation for this).

image

From here you can build more complicated types (an abstract syntax tree is another common example) using these simple building blocks.

 

Emulating in C#

Now, since C# does not have support for these sum (or union) types, what can you do with discriminated union types that you have created in F#?

Turns out, you can do a pretty good approximation in C# using a class hierarchy instead, although you do lose many of the expressiveness (the succinct syntax for declaring them, and the ability to pattern match against them) you get in F#. The good news is that the F# compiler does most of the heavy lifting for you already.

For each of the union types (i.e. variants of the base type, in the case of Tree, the union types are Leaf and Node) the F# compiler generates a static Tree.NewXYZ method for creating a new instance of that union type, and you can check which union type an instance belongs to by either:

  • performing a type test, or
  • using the generated IsXYZ property on every instance of Tree

 

For the Tree type we defined in F#, we can create and traverse it using the following:

(note: in C# tuple members are accessed by the ItemX properties where X is the 1-based index of the element.)

 

What has happened behind the scene is that the F# compiler has transformed the Tree type we defined into a simple class hierarchy consisting of:

  • An abstract Tree<T> type
  • Two nested types Leaf and Node which extends from Tree<T>

Peeping at the IL generated by the F# compiler through ILSpy you will find something along the lines of:

image

 

Emulating in Dart

Now, unfortunately there’s no native support for sum (or union) types in Dart either, but as we saw in the C# case, you can do a pretty good approximation even without it. As an example, here’s how you might define the Tree type in Dart:

In this example, we have an abstract (so you can’t instantiate it with the new keyword) Tree type which is extended by the union types Leaf and Node. Because both Leaf and Node specifies only a private constructor (as denoted by the _ prefix to its named constructor) so the only way to instantiate them is through the static Tree.NewLeaf and Tree.NewNode methods.

Also, note that the private constructors for Leaf and Node calls Tree’s private constructor in order to initialize the isLeaf and isNode public fields.

To use this Tree type, our code is very similar to the C# example:

Here, we need to check whether a given Tree instance is a Leaf or a Node and cast it to the appropriate type (using our helper methods) before we can access the relevant data for each of the union types.

 

On a side note, I stumbled across an interesting library for defining extractors in Dart, so that you get similar pattern matching capabilities as those found in a number of functional languages such as F# and Scala.

 

Related Links

F# – Enums vs Discriminated Unions

F# – Serializing F# record and Discriminated Union types

F# – Use Discriminated Unions instead of class hierarchies

SO – what are ‘sums-and-products’ data structures?

Pattern Matching Combinators for Dart

stream_ext – version 0.3.0 is out

I have just published version 0.3.0 of stream_ext, my attempt to port the Rx APIs to Dart. In this version I have added a number of additional methods to the existing set of:

 

amb

StreamExt.amb has the following signature:

Stream amb(Stream stream1, Stream stream2, { bool closeOnError : false, bool sync : false })

this method propagates values from the stream that reacts first with a value.

This method will ignore any errors received from either stream until the first value is received. The stream which reacts first with a value will have its values and errors propagated through the output stream.

The output stream will complete if:

  • neither stream produced a value before completing
  • the propagated stream has completed
  • the closeOnError flag is set to true and an error is received in the propagated stream

 

Marble Diagram

 

Live demo here.

More information on the Observable.amb extension method from Rx API here.

 

log

StreamExt.log has the following signature:

void log(Stream input, [ String prefix, void log(Object msg) ])

this helper method provide an easy way to log when new values and errors are received and when the stream is done.

You can see example usage here.

 

onErrorResumeNext

StreamExt.onErrorResumeNext has the following signature:

Stream onErrorResumeNext(Stream stream1, Stream stream2, { bool closeOnError : false, bool sync : false })

this method allows the continuation of a stream with another regardless of whether the first stream completes gracefully or due to an error.

The output stream will complete if:

  • both input streams have completed (if stream 2 completes before stream 1 then the output stream is completed when stream 1 completes)
  • closeOnError flag is set to true and an error is received in the continuation stream

 

Marble Diagram

 

Live demo here.

More information on the Observable.onErrorResumeNext extension method from the Rx API here.

 

switchFrom

StreamExt.switchFrom has the following signature:

Stream switchFrom(Stream<Stream> inputs, { bool closeOnError : false, bool sync : false })

this method transforms a stream of streams into a stream producing values only from the most recent stream.

The output stream will complete if:

  • the input stream has completed and the last stream has completed
  • closeOnError flag is set to true and an error is received in the active stream

 

Marble Diagram

 

Live demo here.

More information on the Observable.Switch extension method from the Rx API here.

 

timeOut

StreamExt.timeOut has the following signature:

Stream timeOut(Stream input, Duration duration, { bool closeOnError : false, bool sync : false })

this method allows you to terminate a stream with a TimeoutError if the specified duration between values elapsed.

The output stream will complete if:

  • the input stream has completed
  • the specified duration between input values has elapsed
  • closeOnError flag is set to true and an error is received

 

Marble Diagram

 

Live demo here.

More information on the Observable.Timeout extension method from the Rx API here.

 

timeOutAt

StreamExt.timeOutAt has the following signature:

Stream timeOutAt(Stream input, DateTime dueTime, { bool closeOnError : false, bool sync : false })

this method allows you to terminate a stream with a TimeoutError at the specified dueTime.

The output stream will complete if:

  • the input stream has completed
  • the specified dueTime has elapsed
  • closeOnError flag is set to true and an error is received

 

Marble Diagram

 

Live demo here.

 

Links

Source Code

API Ref­er­ence

Get­ting Started guide

Wiki (with live demo for each method)

Intro to Rx

stream_ext – version 0.2.0 is out

Lately I’ve been making steady progress in porting over Rx APIs over to Dart with stream_ext, and with the release of version 0.2.0 a few more Rx methods have been added to the existing set of buffer, combineLatest, delay, max, merge, min, scan, sum, throttle, window and zip.

 

average

StreamExt.average has the following signature:

Future average(Stream input, { num map (dynamic elem), bool closeOnError : false, bool sync : false })

this method returns the average of the input values as a Future which completes when the input stream is done.

This method uses the supplied map function to convert each input value into a num. If a map function is not specified then the identity function is used instead.

If closeOnError flag is set to true, then any error in the map function or from the input stream will complete the Future with the error. Otherwise, any errors will be swallowed and excluded from the final average.

 

Live demo here.

 

concat

StreamExt.concat has the following signature:

Stream concat(Stream stream1, Stream stream2, { bool closeOnError : false, bool sync : false })

this method concatenates two streams together, when the first stream completes the second stream is subscribed to. Until the first stream is done any values and errors from the second stream is ignored.

The concatenated stream will complete if:

  • both input streams have completed (if stream 2 completes before stream 1 then the concatenated stream is completed when stream 1 completes)
  • closeOnError flag is set to true and an error is received in the active input stream

marble diagram

 

Live demo here.

 

repeat

StreamExt.repeat has the following signature:

Stream repeat(Stream input, { int repeatCount, bool closeOnError : false, bool sync : false })

this method allows you to repeat the input stream for the specified number of times. If repeatCount is not set, then the input stream will be repeated indefinitely. The repeated stream respects both the order and timing of values from the input stream.

The done value is not delivered when the input stream completes, but only after the input stream has been repeated the required number of times.

The output stream will complete if:

  • the input stream has been repeated the required number of times
  • the closeOnError flag is set to true and an error has been received

marble diagram

 

Live demo here.

 

sample

StreamExt.sample has the following signature:

Stream sample(Stream input, Duration duration, { bool closeOnError : false, bool sync : false })

this method creates a new stream by taking the last value from the input stream for every specified duration.

The sampled stream will complete if:

  • the input stream has completed and any sampled message has been delivered
  • the closeOnError flag is set to true and an error has been received

marble diagram

 

Live demo here.

 

startWith

StreamExt.startWith has the following signature:

Stream startWith(Stream input, Iterable values, { bool closeOnError : false, bool sync : false })

this method allows you to prefix values to a stream. The supplied values are delivered as soon as the listener is subscribed before the listener receives values from the input stream.

The output stream will complete if:

  • the input stream has completed
  • closeOnError flag is set to true and an error is received

marble diagram

 

Live demo here.

 

Once again, I hope you find these methods useful to you and please don’t hesitate to send me any questions or feedbacks you have about the project.

 

Links

Source Code

API Reference

Getting Started guide

Wiki (with live demo for each method)

Intro to Rx