Add the ability to disable chain-tracking. (#17)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7182baf..5d25eac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 1.7.0
+
+* Add a `Chain.disable()` function that disables stack-chain tracking.
+
+* Fix a bug where `Chain.capture(..., when: false)` would throw if an error was
+  emitted without a stack trace.
+
 ## 1.6.8
 
 * Add a note to the documentation of `Chain.terse` and `Trace.terse`.
diff --git a/lib/src/chain.dart b/lib/src/chain.dart
index 3d1e1fc..7045e19 100644
--- a/lib/src/chain.dart
+++ b/lib/src/chain.dart
@@ -14,6 +14,9 @@
 @Deprecated("Will be removed in stack_trace 2.0.0.")
 typedef void ChainHandler(error, Chain chain);
 
+/// An opaque key used to track the current [StackZoneSpecification].
+final _specKey = new Object();
+
 /// A chain of stack traces.
 ///
 /// A stack chain is a collection of one or more stack traces that collectively
@@ -43,8 +46,7 @@
   final List<Trace> traces;
 
   /// The [StackZoneSpecification] for the current zone.
-  static StackZoneSpecification get _currentSpec =>
-    Zone.current[#stack_trace.stack_zone.spec];
+  static StackZoneSpecification get _currentSpec => Zone.current[_specKey];
 
   /// If [when] is `true`, runs [callback] in a [Zone] in which the current
   /// stack chain is tracked and automatically associated with (most) errors.
@@ -73,7 +75,11 @@
       var newOnError;
       if (onError != null) {
         newOnError = (error, stackTrace) {
-          onError(error, new Chain.forTrace(stackTrace));
+          onError(
+              error,
+              stackTrace == null
+                  ? new Chain.current()
+                  : new Chain.forTrace(stackTrace));
         };
       }
 
@@ -89,11 +95,27 @@
         return Zone.current.handleUncaughtError(error, stackTrace);
       }
     }, zoneSpecification: spec.toSpec(), zoneValues: {
-      #stack_trace.stack_zone.spec: spec
+      _specKey: spec,
+      StackZoneSpecification.disableKey: false
     }) as dynamic/*=T*/;
     // TODO(rnystrom): Remove this cast if runZoned() gets a generic type.
   }
 
+  /// If [when] is `true` and this is called within a [Chain.capture] zone, runs
+  /// [callback] in a [Zone] in which chain capturing is disabled.
+  ///
+  /// If [callback] returns a value, it will be returned by [disable] as well.
+  static /*=T*/ disable/*<T>*/(/*=T*/ callback(), {bool when: true}) {
+    var zoneValues = when
+        ? {
+            _specKey: null,
+            StackZoneSpecification.disableKey: true
+          }
+        : null;
+
+    return runZoned(callback, zoneValues: zoneValues);
+  }
+
   /// Returns [futureOrStream] unmodified.
   ///
   /// Prior to Dart 1.7, this was necessary to ensure that stack traces for
diff --git a/lib/src/stack_zone_specification.dart b/lib/src/stack_zone_specification.dart
index 3a9bf48..7125249 100644
--- a/lib/src/stack_zone_specification.dart
+++ b/lib/src/stack_zone_specification.dart
@@ -30,6 +30,15 @@
 /// Since [ZoneSpecification] can't be extended or even implemented, in order to
 /// get a real [ZoneSpecification] instance it's necessary to call [toSpec].
 class StackZoneSpecification {
+  /// An opaque object used as a zone value to disable chain tracking in a given
+  /// zone.
+  ///
+  /// If `Zone.current[disableKey]` is `true`, no stack chains will be tracked.
+  static final disableKey = new Object();
+
+  /// Whether chain-tracking is disabled in the current zone.
+  bool get _disabled => Zone.current[disableKey] == true;
+
   /// The expando that associates stack chains with [StackTrace]s.
   ///
   /// The chains are associated with stack traces rather than errors themselves
@@ -54,11 +63,11 @@
   /// Converts [this] to a real [ZoneSpecification].
   ZoneSpecification toSpec() {
     return new ZoneSpecification(
-        handleUncaughtError: handleUncaughtError,
-        registerCallback: registerCallback,
-        registerUnaryCallback: registerUnaryCallback,
-        registerBinaryCallback: registerBinaryCallback,
-        errorCallback: errorCallback);
+        handleUncaughtError: _handleUncaughtError,
+        registerCallback: _registerCallback,
+        registerUnaryCallback: _registerUnaryCallback,
+        registerBinaryCallback: _registerBinaryCallback,
+        errorCallback: _errorCallback);
   }
 
   /// Returns the current stack chain.
@@ -79,57 +88,20 @@
     return new _Node(trace, previous).toChain();
   }
 
-  /// Ensures that an error emitted by [future] has the correct stack
-  /// information associated with it.
-  ///
-  /// By default, the first frame of the first trace will be the line where
-  /// [trackFuture] is called. If [level] is passed, the first trace will start
-  /// that many frames up instead.
-  Future trackFuture(Future future, [int level=0]) {
-    var completer = new Completer.sync();
-    var node = _createNode(level + 1);
-    future.then(completer.complete).catchError((e, stackTrace) {
-      if (stackTrace == null) stackTrace = new Trace.current();
-      if (stackTrace is! Chain && _chains[stackTrace] == null) {
-        _chains[stackTrace] = node;
-      }
-      completer.completeError(e, stackTrace);
-    });
-    return completer.future;
-  }
-
-  /// Ensures that any errors emitted by [stream] have the correct stack
-  /// information associated with them.
-  ///
-  /// By default, the first frame of the first trace will be the line where
-  /// [trackStream] is called. If [level] is passed, the first trace will start
-  /// that many frames up instead.
-  Stream trackStream(Stream stream, [int level=0]) {
-    var node = _createNode(level + 1);
-    return stream.transform(new StreamTransformer.fromHandlers(
-        handleError: (error, stackTrace, sink) {
-      if (stackTrace == null) stackTrace = new Trace.current();
-      if (stackTrace is! Chain && _chains[stackTrace] == null) {
-        _chains[stackTrace] = node;
-      }
-      sink.addError(error, stackTrace);
-    }));
-  }
-
   /// Tracks the current stack chain so it can be set to [_currentChain] when
   /// [f] is run.
-  ZoneCallback registerCallback(Zone self, ZoneDelegate parent, Zone zone,
+  ZoneCallback _registerCallback(Zone self, ZoneDelegate parent, Zone zone,
       Function f) {
-    if (f == null) return parent.registerCallback(zone, null);
+    if (f == null || _disabled) return parent.registerCallback(zone, f);
     var node = _createNode(1);
     return parent.registerCallback(zone, () => _run(f, node));
   }
 
   /// Tracks the current stack chain so it can be set to [_currentChain] when
   /// [f] is run.
-  ZoneUnaryCallback registerUnaryCallback(Zone self, ZoneDelegate parent,
+  ZoneUnaryCallback _registerUnaryCallback(Zone self, ZoneDelegate parent,
       Zone zone, Function f) {
-    if (f == null) return parent.registerUnaryCallback(zone, null);
+    if (f == null || _disabled) return parent.registerUnaryCallback(zone, f);
     var node = _createNode(1);
     return parent.registerUnaryCallback(zone, (arg) {
       return _run(() => f(arg), node);
@@ -138,9 +110,10 @@
 
   /// Tracks the current stack chain so it can be set to [_currentChain] when
   /// [f] is run.
-  ZoneBinaryCallback registerBinaryCallback(Zone self, ZoneDelegate parent,
+  ZoneBinaryCallback _registerBinaryCallback(Zone self, ZoneDelegate parent,
       Zone zone, Function f) {
-    if (f == null) return parent.registerBinaryCallback(zone, null);
+    if (f == null || _disabled) return parent.registerBinaryCallback(zone, f);
+
     var node = _createNode(1);
     return parent.registerBinaryCallback(zone, (arg1, arg2) {
       return _run(() => f(arg1, arg2), node);
@@ -149,8 +122,12 @@
 
   /// Looks up the chain associated with [stackTrace] and passes it either to
   /// [_onError] or [parent]'s error handler.
-  handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone, error,
+  _handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone, error,
       StackTrace stackTrace) {
+    if (_disabled) {
+      return parent.handleUncaughtError(zone, error, stackTrace);
+    }
+
     var stackChain = chainFor(stackTrace);
     if (_onError == null) {
       return parent.handleUncaughtError(zone, error, stackChain);
@@ -171,8 +148,10 @@
 
   /// Attaches the current stack chain to [stackTrace], replacing it if
   /// necessary.
-  AsyncError errorCallback(Zone self, ZoneDelegate parent, Zone zone,
+  AsyncError _errorCallback(Zone self, ZoneDelegate parent, Zone zone,
       Object error, StackTrace stackTrace) {
+    if (_disabled) return parent.errorCallback(zone, error, stackTrace);
+
     // Go up two levels to get through [_CustomZone.errorCallback].
     if (stackTrace == null) {
       stackTrace = _createNode(2).toChain();
diff --git a/pubspec.yaml b/pubspec.yaml
index ff5a3c1..6984445 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@
 #
 # When the major version is upgraded, you *must* update that version constraint
 # in pub to stay in sync with this.
-version: 1.6.8
+version: 1.7.0
 author: "Dart Team <misc@dartlang.org>"
 homepage: https://github.com/dart-lang/stack_trace
 description: >
diff --git a/test/chain/chain_test.dart b/test/chain/chain_test.dart
index 0824f4f..301f338 100644
--- a/test/chain/chain_test.dart
+++ b/test/chain/chain_test.dart
@@ -8,6 +8,7 @@
 import 'package:stack_trace/stack_trace.dart';
 import 'package:test/test.dart';
 
+import '../utils.dart';
 import 'utils.dart';
 
 typedef void ChainErrorCallback(stack, Chain chain);
@@ -53,6 +54,82 @@
       }) as ChainErrorCallback, when: false);
       // TODO(rnystrom): Remove this cast if expectAsync() gets a better type.
     });
+
+    test("doesn't enable chain-tracking", () {
+      return Chain.disable(() {
+        return Chain.capture(() {
+          var completer = new Completer();
+          inMicrotask(() {
+            completer.complete(new Chain.current());
+          });
+
+          return completer.future.then((chain) {
+            expect(chain.traces, hasLength(1));
+          });
+        }, when: false);
+      });
+    });
+  });
+
+  group("Chain.disable()", () {
+    test("disables chain-tracking", () {
+      return Chain.disable(() {
+        var completer = new Completer();
+        inMicrotask(() => completer.complete(new Chain.current()));
+
+        return completer.future.then((chain) {
+          expect(chain.traces, hasLength(1));
+        });
+      });
+    });
+
+    test("Chain.capture() re-enables chain-tracking", () {
+      return Chain.disable(() {
+        return Chain.capture(() {
+          var completer = new Completer();
+          inMicrotask(() => completer.complete(new Chain.current()));
+
+          return completer.future.then((chain) {
+            expect(chain.traces, hasLength(2));
+          });
+        });
+      });
+    });
+
+    test("preserves parent zones of the capture zone", () {
+      // The outer disable call turns off the test package's chain-tracking.
+      return Chain.disable(() {
+        return runZoned(() {
+          return Chain.capture(() {
+            expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+          });
+        }, zoneValues: {#enabled: true});
+      });
+    });
+
+    test("preserves child zones of the capture zone", () {
+      // The outer disable call turns off the test package's chain-tracking.
+      return Chain.disable(() {
+        return Chain.capture(() {
+          return runZoned(() {
+            expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+          }, zoneValues: {#enabled: true});
+        });
+      });
+    });
+
+    test("with when: false doesn't disable", () {
+      return Chain.capture(() {
+        return Chain.disable(() {
+          var completer = new Completer();
+          inMicrotask(() => completer.complete(new Chain.current()));
+
+          return completer.future.then((chain) {
+            expect(chain.traces, hasLength(2));
+          });
+        }, when: false);
+      });
+    });
   });
 
   test("toString() ensures that all traces are aligned", () {
@@ -248,68 +325,4 @@
         '$userSlashCode 10:11  Foo.bar\n'
         'dart:core 10:11       Bar.baz\n'));
   });
-
-  group('Chain.track(Future)', () {
-    test('forwards the future value within Chain.capture()', () {
-      Chain.capture(() {
-        expect(Chain.track(new Future.value('value')),
-            completion(equals('value')));
-
-        var trace = new Trace.current();
-        expect(Chain.track(new Future.error('error', trace))
-            .catchError((e, stackTrace) {
-          expect(e, equals('error'));
-          expect(stackTrace.toString(), equals(trace.toString()));
-        }), completes);
-      });
-    });
-
-    test('forwards the future value outside of Chain.capture()', () {
-      expect(Chain.track(new Future.value('value')),
-          completion(equals('value')));
-
-      var trace = new Trace.current();
-      expect(Chain.track(new Future.error('error', trace))
-          .catchError((e, stackTrace) {
-        expect(e, equals('error'));
-        expect(stackTrace.toString(), equals(trace.toString()));
-      }), completes);
-    });
-  });
-
-  group('Chain.track(Stream)', () {
-    test('forwards stream values within Chain.capture()', () {
-      Chain.capture(() {
-        var controller = new StreamController()
-            ..add(1)..add(2)..add(3)..close();
-        expect(Chain.track(controller.stream).toList(),
-            completion(equals([1, 2, 3])));
-
-        var trace = new Trace.current();
-        controller = new StreamController()..addError('error', trace);
-        expect(Chain.track(controller.stream).toList()
-            .catchError((e, stackTrace) {
-          expect(e, equals('error'));
-          expect(stackTrace.toString(), equals(trace.toString()));
-        }), completes);
-      });
-    });
-
-    test('forwards stream values outside of Chain.capture()', () {
-      Chain.capture(() {
-        var controller = new StreamController()
-            ..add(1)..add(2)..add(3)..close();
-        expect(Chain.track(controller.stream).toList(),
-            completion(equals([1, 2, 3])));
-
-        var trace = new Trace.current();
-        controller = new StreamController()..addError('error', trace);
-        expect(Chain.track(controller.stream).toList()
-            .catchError((e, stackTrace) {
-          expect(e, equals('error'));
-          expect(stackTrace.toString(), equals(trace.toString()));
-        }), completes);
-      });
-    });
-  });
 }
\ No newline at end of file
diff --git a/test/chain/dart2js_test.dart b/test/chain/dart2js_test.dart
index afb27fa..b8d3a82 100644
--- a/test/chain/dart2js_test.dart
+++ b/test/chain/dart2js_test.dart
@@ -240,9 +240,8 @@
 
   test('current() outside of capture() returns a chain wrapping the current '
       'trace', () {
-    // The test runner runs all tests with chains enabled, so to test without we
-    // have to do some zone munging.
-    return runZoned(() async {
+    // The test runner runs all tests with chains enabled.
+    return Chain.disable(() async {
       var completer = new Completer();
       inMicrotask(() => completer.complete(new Chain.current()));
 
@@ -251,7 +250,7 @@
       // chain isn't available and it just returns the current stack when
       // called.
       expect(chain.traces, hasLength(1));
-    }, zoneValues: {#stack_trace.stack_zone.spec: null});
+    });
   });
 
   group('forTrace() within capture()', () {
diff --git a/test/chain/vm_test.dart b/test/chain/vm_test.dart
index 70635b7..716b146 100644
--- a/test/chain/vm_test.dart
+++ b/test/chain/vm_test.dart
@@ -351,9 +351,8 @@
 
   test('current() outside of capture() returns a chain wrapping the current '
       'trace', () {
-    // The test runner runs all tests with chains enabled, so to test without we
-    // have to do some zone munging.
-    return runZoned(() {
+    // The test runner runs all tests with chains enabled.
+    return Chain.disable(() {
       var completer = new Completer();
       inMicrotask(() => completer.complete(new Chain.current()));
 
@@ -365,7 +364,7 @@
         expect(chain.traces.first.frames.first,
             frameMember(startsWith('main')));
       });
-    }, zoneValues: {#stack_trace.stack_zone.spec: null});
+    });
   });
 
   group('forTrace() within capture()', () {