Skip to content

Commit e75279d

Browse files
authored
Add the ability to disable chain-tracking. (flutter#17)
1 parent a56a284 commit e75279d

File tree

7 files changed

+146
-127
lines changed

7 files changed

+146
-127
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 1.7.0
2+
3+
* Add a `Chain.disable()` function that disables stack-chain tracking.
4+
5+
* Fix a bug where `Chain.capture(..., when: false)` would throw if an error was
6+
emitted without a stack trace.
7+
18
## 1.6.8
29

310
* Add a note to the documentation of `Chain.terse` and `Trace.terse`.

lib/src/chain.dart

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import 'utils.dart';
1414
@Deprecated("Will be removed in stack_trace 2.0.0.")
1515
typedef void ChainHandler(error, Chain chain);
1616

17+
/// An opaque key used to track the current [StackZoneSpecification].
18+
final _specKey = new Object();
19+
1720
/// A chain of stack traces.
1821
///
1922
/// A stack chain is a collection of one or more stack traces that collectively
@@ -43,8 +46,7 @@ class Chain implements StackTrace {
4346
final List<Trace> traces;
4447

4548
/// The [StackZoneSpecification] for the current zone.
46-
static StackZoneSpecification get _currentSpec =>
47-
Zone.current[#stack_trace.stack_zone.spec];
49+
static StackZoneSpecification get _currentSpec => Zone.current[_specKey];
4850

4951
/// If [when] is `true`, runs [callback] in a [Zone] in which the current
5052
/// stack chain is tracked and automatically associated with (most) errors.
@@ -73,7 +75,11 @@ class Chain implements StackTrace {
7375
var newOnError;
7476
if (onError != null) {
7577
newOnError = (error, stackTrace) {
76-
onError(error, new Chain.forTrace(stackTrace));
78+
onError(
79+
error,
80+
stackTrace == null
81+
? new Chain.current()
82+
: new Chain.forTrace(stackTrace));
7783
};
7884
}
7985

@@ -89,11 +95,27 @@ class Chain implements StackTrace {
8995
return Zone.current.handleUncaughtError(error, stackTrace);
9096
}
9197
}, zoneSpecification: spec.toSpec(), zoneValues: {
92-
#stack_trace.stack_zone.spec: spec
98+
_specKey: spec,
99+
StackZoneSpecification.disableKey: false
93100
}) as dynamic/*=T*/;
94101
// TODO(rnystrom): Remove this cast if runZoned() gets a generic type.
95102
}
96103

104+
/// If [when] is `true` and this is called within a [Chain.capture] zone, runs
105+
/// [callback] in a [Zone] in which chain capturing is disabled.
106+
///
107+
/// If [callback] returns a value, it will be returned by [disable] as well.
108+
static /*=T*/ disable/*<T>*/(/*=T*/ callback(), {bool when: true}) {
109+
var zoneValues = when
110+
? {
111+
_specKey: null,
112+
StackZoneSpecification.disableKey: true
113+
}
114+
: null;
115+
116+
return runZoned(callback, zoneValues: zoneValues);
117+
}
118+
97119
/// Returns [futureOrStream] unmodified.
98120
///
99121
/// Prior to Dart 1.7, this was necessary to ensure that stack traces for

lib/src/stack_zone_specification.dart

Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ typedef void _ChainHandler(error, Chain chain);
3030
/// Since [ZoneSpecification] can't be extended or even implemented, in order to
3131
/// get a real [ZoneSpecification] instance it's necessary to call [toSpec].
3232
class StackZoneSpecification {
33+
/// An opaque object used as a zone value to disable chain tracking in a given
34+
/// zone.
35+
///
36+
/// If `Zone.current[disableKey]` is `true`, no stack chains will be tracked.
37+
static final disableKey = new Object();
38+
39+
/// Whether chain-tracking is disabled in the current zone.
40+
bool get _disabled => Zone.current[disableKey] == true;
41+
3342
/// The expando that associates stack chains with [StackTrace]s.
3443
///
3544
/// The chains are associated with stack traces rather than errors themselves
@@ -54,11 +63,11 @@ class StackZoneSpecification {
5463
/// Converts [this] to a real [ZoneSpecification].
5564
ZoneSpecification toSpec() {
5665
return new ZoneSpecification(
57-
handleUncaughtError: handleUncaughtError,
58-
registerCallback: registerCallback,
59-
registerUnaryCallback: registerUnaryCallback,
60-
registerBinaryCallback: registerBinaryCallback,
61-
errorCallback: errorCallback);
66+
handleUncaughtError: _handleUncaughtError,
67+
registerCallback: _registerCallback,
68+
registerUnaryCallback: _registerUnaryCallback,
69+
registerBinaryCallback: _registerBinaryCallback,
70+
errorCallback: _errorCallback);
6271
}
6372

6473
/// Returns the current stack chain.
@@ -79,57 +88,20 @@ class StackZoneSpecification {
7988
return new _Node(trace, previous).toChain();
8089
}
8190

82-
/// Ensures that an error emitted by [future] has the correct stack
83-
/// information associated with it.
84-
///
85-
/// By default, the first frame of the first trace will be the line where
86-
/// [trackFuture] is called. If [level] is passed, the first trace will start
87-
/// that many frames up instead.
88-
Future trackFuture(Future future, [int level=0]) {
89-
var completer = new Completer.sync();
90-
var node = _createNode(level + 1);
91-
future.then(completer.complete).catchError((e, stackTrace) {
92-
if (stackTrace == null) stackTrace = new Trace.current();
93-
if (stackTrace is! Chain && _chains[stackTrace] == null) {
94-
_chains[stackTrace] = node;
95-
}
96-
completer.completeError(e, stackTrace);
97-
});
98-
return completer.future;
99-
}
100-
101-
/// Ensures that any errors emitted by [stream] have the correct stack
102-
/// information associated with them.
103-
///
104-
/// By default, the first frame of the first trace will be the line where
105-
/// [trackStream] is called. If [level] is passed, the first trace will start
106-
/// that many frames up instead.
107-
Stream trackStream(Stream stream, [int level=0]) {
108-
var node = _createNode(level + 1);
109-
return stream.transform(new StreamTransformer.fromHandlers(
110-
handleError: (error, stackTrace, sink) {
111-
if (stackTrace == null) stackTrace = new Trace.current();
112-
if (stackTrace is! Chain && _chains[stackTrace] == null) {
113-
_chains[stackTrace] = node;
114-
}
115-
sink.addError(error, stackTrace);
116-
}));
117-
}
118-
11991
/// Tracks the current stack chain so it can be set to [_currentChain] when
12092
/// [f] is run.
121-
ZoneCallback registerCallback(Zone self, ZoneDelegate parent, Zone zone,
93+
ZoneCallback _registerCallback(Zone self, ZoneDelegate parent, Zone zone,
12294
Function f) {
123-
if (f == null) return parent.registerCallback(zone, null);
95+
if (f == null || _disabled) return parent.registerCallback(zone, f);
12496
var node = _createNode(1);
12597
return parent.registerCallback(zone, () => _run(f, node));
12698
}
12799

128100
/// Tracks the current stack chain so it can be set to [_currentChain] when
129101
/// [f] is run.
130-
ZoneUnaryCallback registerUnaryCallback(Zone self, ZoneDelegate parent,
102+
ZoneUnaryCallback _registerUnaryCallback(Zone self, ZoneDelegate parent,
131103
Zone zone, Function f) {
132-
if (f == null) return parent.registerUnaryCallback(zone, null);
104+
if (f == null || _disabled) return parent.registerUnaryCallback(zone, f);
133105
var node = _createNode(1);
134106
return parent.registerUnaryCallback(zone, (arg) {
135107
return _run(() => f(arg), node);
@@ -138,9 +110,10 @@ class StackZoneSpecification {
138110

139111
/// Tracks the current stack chain so it can be set to [_currentChain] when
140112
/// [f] is run.
141-
ZoneBinaryCallback registerBinaryCallback(Zone self, ZoneDelegate parent,
113+
ZoneBinaryCallback _registerBinaryCallback(Zone self, ZoneDelegate parent,
142114
Zone zone, Function f) {
143-
if (f == null) return parent.registerBinaryCallback(zone, null);
115+
if (f == null || _disabled) return parent.registerBinaryCallback(zone, f);
116+
144117
var node = _createNode(1);
145118
return parent.registerBinaryCallback(zone, (arg1, arg2) {
146119
return _run(() => f(arg1, arg2), node);
@@ -149,8 +122,12 @@ class StackZoneSpecification {
149122

150123
/// Looks up the chain associated with [stackTrace] and passes it either to
151124
/// [_onError] or [parent]'s error handler.
152-
handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone, error,
125+
_handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone, error,
153126
StackTrace stackTrace) {
127+
if (_disabled) {
128+
return parent.handleUncaughtError(zone, error, stackTrace);
129+
}
130+
154131
var stackChain = chainFor(stackTrace);
155132
if (_onError == null) {
156133
return parent.handleUncaughtError(zone, error, stackChain);
@@ -171,8 +148,10 @@ class StackZoneSpecification {
171148

172149
/// Attaches the current stack chain to [stackTrace], replacing it if
173150
/// necessary.
174-
AsyncError errorCallback(Zone self, ZoneDelegate parent, Zone zone,
151+
AsyncError _errorCallback(Zone self, ZoneDelegate parent, Zone zone,
175152
Object error, StackTrace stackTrace) {
153+
if (_disabled) return parent.errorCallback(zone, error, stackTrace);
154+
176155
// Go up two levels to get through [_CustomZone.errorCallback].
177156
if (stackTrace == null) {
178157
stackTrace = _createNode(2).toChain();

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name: stack_trace
77
#
88
# When the major version is upgraded, you *must* update that version constraint
99
# in pub to stay in sync with this.
10-
version: 1.6.8
10+
version: 1.7.0
1111
author: "Dart Team <[email protected]>"
1212
homepage: https://github.com/dart-lang/stack_trace
1313
description: >

test/chain/chain_test.dart

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:path/path.dart' as p;
88
import 'package:stack_trace/stack_trace.dart';
99
import 'package:test/test.dart';
1010

11+
import '../utils.dart';
1112
import 'utils.dart';
1213

1314
typedef void ChainErrorCallback(stack, Chain chain);
@@ -53,6 +54,82 @@ void main() {
5354
}) as ChainErrorCallback, when: false);
5455
// TODO(rnystrom): Remove this cast if expectAsync() gets a better type.
5556
});
57+
58+
test("doesn't enable chain-tracking", () {
59+
return Chain.disable(() {
60+
return Chain.capture(() {
61+
var completer = new Completer();
62+
inMicrotask(() {
63+
completer.complete(new Chain.current());
64+
});
65+
66+
return completer.future.then((chain) {
67+
expect(chain.traces, hasLength(1));
68+
});
69+
}, when: false);
70+
});
71+
});
72+
});
73+
74+
group("Chain.disable()", () {
75+
test("disables chain-tracking", () {
76+
return Chain.disable(() {
77+
var completer = new Completer();
78+
inMicrotask(() => completer.complete(new Chain.current()));
79+
80+
return completer.future.then((chain) {
81+
expect(chain.traces, hasLength(1));
82+
});
83+
});
84+
});
85+
86+
test("Chain.capture() re-enables chain-tracking", () {
87+
return Chain.disable(() {
88+
return Chain.capture(() {
89+
var completer = new Completer();
90+
inMicrotask(() => completer.complete(new Chain.current()));
91+
92+
return completer.future.then((chain) {
93+
expect(chain.traces, hasLength(2));
94+
});
95+
});
96+
});
97+
});
98+
99+
test("preserves parent zones of the capture zone", () {
100+
// The outer disable call turns off the test package's chain-tracking.
101+
return Chain.disable(() {
102+
return runZoned(() {
103+
return Chain.capture(() {
104+
expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
105+
});
106+
}, zoneValues: {#enabled: true});
107+
});
108+
});
109+
110+
test("preserves child zones of the capture zone", () {
111+
// The outer disable call turns off the test package's chain-tracking.
112+
return Chain.disable(() {
113+
return Chain.capture(() {
114+
return runZoned(() {
115+
expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
116+
}, zoneValues: {#enabled: true});
117+
});
118+
});
119+
});
120+
121+
test("with when: false doesn't disable", () {
122+
return Chain.capture(() {
123+
return Chain.disable(() {
124+
var completer = new Completer();
125+
inMicrotask(() => completer.complete(new Chain.current()));
126+
127+
return completer.future.then((chain) {
128+
expect(chain.traces, hasLength(2));
129+
});
130+
}, when: false);
131+
});
132+
});
56133
});
57134

58135
test("toString() ensures that all traces are aligned", () {
@@ -248,68 +325,4 @@ void main() {
248325
'$userSlashCode 10:11 Foo.bar\n'
249326
'dart:core 10:11 Bar.baz\n'));
250327
});
251-
252-
group('Chain.track(Future)', () {
253-
test('forwards the future value within Chain.capture()', () {
254-
Chain.capture(() {
255-
expect(Chain.track(new Future.value('value')),
256-
completion(equals('value')));
257-
258-
var trace = new Trace.current();
259-
expect(Chain.track(new Future.error('error', trace))
260-
.catchError((e, stackTrace) {
261-
expect(e, equals('error'));
262-
expect(stackTrace.toString(), equals(trace.toString()));
263-
}), completes);
264-
});
265-
});
266-
267-
test('forwards the future value outside of Chain.capture()', () {
268-
expect(Chain.track(new Future.value('value')),
269-
completion(equals('value')));
270-
271-
var trace = new Trace.current();
272-
expect(Chain.track(new Future.error('error', trace))
273-
.catchError((e, stackTrace) {
274-
expect(e, equals('error'));
275-
expect(stackTrace.toString(), equals(trace.toString()));
276-
}), completes);
277-
});
278-
});
279-
280-
group('Chain.track(Stream)', () {
281-
test('forwards stream values within Chain.capture()', () {
282-
Chain.capture(() {
283-
var controller = new StreamController()
284-
..add(1)..add(2)..add(3)..close();
285-
expect(Chain.track(controller.stream).toList(),
286-
completion(equals([1, 2, 3])));
287-
288-
var trace = new Trace.current();
289-
controller = new StreamController()..addError('error', trace);
290-
expect(Chain.track(controller.stream).toList()
291-
.catchError((e, stackTrace) {
292-
expect(e, equals('error'));
293-
expect(stackTrace.toString(), equals(trace.toString()));
294-
}), completes);
295-
});
296-
});
297-
298-
test('forwards stream values outside of Chain.capture()', () {
299-
Chain.capture(() {
300-
var controller = new StreamController()
301-
..add(1)..add(2)..add(3)..close();
302-
expect(Chain.track(controller.stream).toList(),
303-
completion(equals([1, 2, 3])));
304-
305-
var trace = new Trace.current();
306-
controller = new StreamController()..addError('error', trace);
307-
expect(Chain.track(controller.stream).toList()
308-
.catchError((e, stackTrace) {
309-
expect(e, equals('error'));
310-
expect(stackTrace.toString(), equals(trace.toString()));
311-
}), completes);
312-
});
313-
});
314-
});
315328
}

test/chain/dart2js_test.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,8 @@ void main() {
240240

241241
test('current() outside of capture() returns a chain wrapping the current '
242242
'trace', () {
243-
// The test runner runs all tests with chains enabled, so to test without we
244-
// have to do some zone munging.
245-
return runZoned(() async {
243+
// The test runner runs all tests with chains enabled.
244+
return Chain.disable(() async {
246245
var completer = new Completer();
247246
inMicrotask(() => completer.complete(new Chain.current()));
248247

@@ -251,7 +250,7 @@ void main() {
251250
// chain isn't available and it just returns the current stack when
252251
// called.
253252
expect(chain.traces, hasLength(1));
254-
}, zoneValues: {#stack_trace.stack_zone.spec: null});
253+
});
255254
});
256255

257256
group('forTrace() within capture()', () {

0 commit comments

Comments
 (0)