Merge branch 'main' into merge-stack_trace-package
diff --git a/.github/ISSUE_TEMPLATE/stack_trace.md b/.github/ISSUE_TEMPLATE/stack_trace.md
new file mode 100644
index 0000000..417362b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stack_trace.md
@@ -0,0 +1,5 @@
+---
+name: "package:stack_trace"
+about: "Create a bug or file a feature request against package:stack_trace."
+labels: "package:stack_trace"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 3ab79c0..25efb2a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -112,6 +112,10 @@
   - changed-files:
     - any-glob-to-any-file: 'pkgs/sse/**'
 
+'package:stack_trace':
+  - changed-files:
+    - any-glob-to-any-file: 'pkgs/stack_trace/**'
+
 'package:stream_transform':
   - changed-files:
     - any-glob-to-any-file: 'pkgs/stream_transform/**'
diff --git a/.github/workflows/stack_trace.yaml b/.github/workflows/stack_trace.yaml
new file mode 100644
index 0000000..7435967
--- /dev/null
+++ b/.github/workflows/stack_trace.yaml
@@ -0,0 +1,75 @@
+name: package:stack_trace
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stack_trace.yaml'
+      - 'pkgs/stack_trace/**'
+  pull_request:
+    branches: [ main ]
+    paths:
+      - '.github/workflows/stack_trace.yaml'
+      - 'pkgs/stack_trace/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/stack_trace/
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release channel: dev
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # Add macos-latest and/or windows-latest if relevant for this package.
+        os: [ubuntu-latest]
+        sdk: [3.4, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run VM tests
+        run: dart test --platform vm
+        if: always() && steps.install.outcome == 'success'
+      - name: Run browser tests
+        run: dart test --platform chrome
+        if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index 0201aa2..563f90d 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,7 @@
 | [source_maps](pkgs/source_maps/) | A library to programmatically manipulate source map files. | [![package issues](https://img.shields.io/badge/package:source_maps-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_maps) | [![pub package](https://img.shields.io/pub/v/source_maps.svg)](https://pub.dev/packages/source_maps) |
 | [source_span](pkgs/source_span/) | Provides a standard representation for source code locations and spans. | [![package issues](https://img.shields.io/badge/package:source_span-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_span) | [![pub package](https://img.shields.io/pub/v/source_span.svg)](https://pub.dev/packages/source_span) |
 | [sse](pkgs/sse/) | Provides client and server functionality for setting up bi-directional communication through Server Sent Events (SSE) and corresponding POST requests. | [![package issues](https://img.shields.io/badge/package:sse-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asse) | [![pub package](https://img.shields.io/pub/v/sse.svg)](https://pub.dev/packages/sse) |
+| [stack_trace](pkgs/stack_trace/) | A package for manipulating stack traces and printing them readably. | [![package issues](https://img.shields.io/badge/package:stack_trace-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astack_trace) | [![pub package](https://img.shields.io/pub/v/stack_trace.svg)](https://pub.dev/packages/stack_trace) |
 | [stream_transform](pkgs/stream_transform/) | A collection of utilities to transform and manipulate streams. | [![package issues](https://img.shields.io/badge/package:stream_transform-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astream_transform) | [![pub package](https://img.shields.io/pub/v/stream_transform.svg)](https://pub.dev/packages/stream_transform) |
 | [term_glyph](pkgs/term_glyph/) | Useful Unicode glyphs and ASCII substitutes. | [![package issues](https://img.shields.io/badge/package:term_glyph-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aterm_glyph) | [![pub package](https://img.shields.io/pub/v/term_glyph.svg)](https://pub.dev/packages/term_glyph) |
 | [test_reflective_loader](pkgs/test_reflective_loader/) | Support for discovering tests and test suites using reflection. | [![package issues](https://img.shields.io/badge/package:test_reflective_loader-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_reflective_loader) | [![pub package](https://img.shields.io/pub/v/test_reflective_loader.svg)](https://pub.dev/packages/test_reflective_loader) |
diff --git a/pkgs/stack_trace/.gitignore b/pkgs/stack_trace/.gitignore
new file mode 100644
index 0000000..f023015
--- /dev/null
+++ b/pkgs/stack_trace/.gitignore
@@ -0,0 +1,6 @@
+# See https://dart.dev/guides/libraries/private-files
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md
new file mode 100644
index 0000000..e92cf9c
--- /dev/null
+++ b/pkgs/stack_trace/CHANGELOG.md
@@ -0,0 +1,363 @@
+## 1.12.1
+
+* Move to `dart-lang/tools` monorepo.
+
+## 1.12.0
+
+* Added support for parsing Wasm frames of Chrome (V8), Firefox, Safari.
+* Require Dart 3.4 or greater
+
+## 1.11.1
+
+* Make use of `@pragma('vm:awaiter-link')` to make package work better with
+  Dart VM's builtin awaiter stack unwinding. No other changes.
+
+## 1.11.0
+
+* Added the parameter `zoneValues` to `Chain.capture` to be able to use custom
+  zone values with the `runZoned` internal calls.
+* Populate the pubspec `repository` field.
+* Require Dart 2.18 or greater
+
+## 1.10.0
+
+* Stable release for null safety.
+* Fix broken test, `test/chain/vm_test.dart`, which incorrectly handles
+  asynchronous suspension gap markers at the end of stack traces.
+
+## 1.10.0-nullsafety.6
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces, when parsing with `Trace.parse` and `Chain.parse`.
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 1.10.0-nullsafety.5
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.10.0-nullsafety.4
+
+* Allow the `2.10.0` stable and dev SDKs.
+
+## 1.10.0-nullsafety.3
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces.
+
+## 1.10.0-nullsafety.2
+
+* Forward fix for a change in SDK type promotion behavior.
+
+## 1.10.0-nullsafety.1
+
+* Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.10.0-nullsafety
+
+* Opt in to null safety.
+
+## 1.9.6 (backpublish)
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces. (Also fixed separately in 1.10.0-nullsafety.3)
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+  traces, when parsing with `Trace.parse` and `Chain.parse`. (Also fixed
+  separately in 1.10.0-nullsafety.6)
+
+## 1.9.5
+
+* Parse the format for `data:` URIs that the Dart VM has used since `2.2.0`.
+
+## 1.9.4
+
+* Add support for firefox anonymous stack traces.
+* Add support for chrome eval stack traces without a column.
+* Change the argument type to `Chain.capture` from `Function(dynamic, Chain)` to
+  `Function(Object, Chain)`. Existing functions which take `dynamic` are still
+  fine, but new uses can have a safer type.
+
+## 1.9.3
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.9.2
+
+* Fix Dart 2.0 runtime cast failure in test.
+
+## 1.9.1
+
+* Preserve the original chain for a trace to handle cases where an
+  error is rethrown.
+
+## 1.9.0
+
+* Add an `errorZone` parameter to `Chain.capture()` that makes it avoid creating
+  an error zone.
+
+## 1.8.3
+
+* `Chain.forTrace()` now returns a full stack chain for *all* `StackTrace`s
+  within `Chain.capture()`, even those that haven't been processed by
+  `dart:async` yet.
+
+* `Chain.forTrace()` now uses the Dart VM's stack chain information when called
+  synchronously within `Chain.capture()`. This matches the existing behavior
+  outside `Chain.capture()`.
+
+* `Chain.forTrace()` now trims the VM's stack chains for the innermost stack
+  trace within `Chain.capture()` (unless it's called synchronously, as above).
+  This avoids duplicated frames and makes the format of the innermost traces
+  consistent with the other traces in the chain.
+
+## 1.8.2
+
+* Update to use strong-mode clean Zone API.
+
+## 1.8.1
+
+* Use official generic function syntax.
+
+* Updated minimum SDK to 1.23.0.
+
+## 1.8.0
+
+* Add a `Trace.original` field to provide access to the original `StackTrace`s
+  from which the `Trace` was created, and a matching constructor parameter to
+  `new Trace()`.
+
+## 1.7.4
+
+* Always run `onError` callbacks for `Chain.capture()` in the parent zone.
+
+## 1.7.3
+
+* Fix broken links in the README.
+
+## 1.7.2
+
+* `Trace.foldFrames()` and `Chain.foldFrames()` now remove the outermost folded
+  frame. This matches the behavior of `.terse` with core frames.
+
+* Fix bug parsing a friendly frame with spaces in the member name.
+
+* Fix bug parsing a friendly frame where the location is a data url.
+
+## 1.7.1
+
+* Make `Trace.parse()`, `Chain.parse()`, treat the VM's new causal asynchronous
+  stack traces as chains. Outside of a `Chain.capture()` block, `new
+  Chain.current()` will return a stack chain constructed from the asynchronous
+  stack traces.
+
+## 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`.
+
+## 1.6.7
+
+* Fix a bug where `new Frame.caller()` returned the wrong depth of frame on
+  Dartium.
+
+## 1.6.6
+
+* `new Trace.current()` and `new Chain.current()` now skip an extra frame when
+  run in a JS context. This makes their return values match the VM context.
+
+## 1.6.5
+
+* Really fix strong mode warnings.
+
+## 1.6.4
+
+* Fix a syntax error introduced in 1.6.3.
+
+## 1.6.3
+
+* Make `Chain.capture()` generic. Its signature is now `T Chain.capture<T>(T
+  callback(), ...)`.
+
+## 1.6.2
+
+* Fix all strong mode warnings.
+
+## 1.6.1
+
+* Use `StackTrace.current` in Dart SDK 1.14 to get the current stack trace.
+
+## 1.6.0
+
+* Add a `when` parameter to `Chain.capture()`. This allows capturing to be
+  easily enabled and disabled based on whether the application is running in
+  debug/development mode or not.
+
+* Deprecate the `ChainHandler` typedef. This didn't provide any value over
+  directly annotating the function argument, and it made the documentation less
+  clear.
+
+## 1.5.1
+
+* Fix a crash in `Chain.foldFrames()` and `Chain.terse` when one of the chain's
+  traces has no frames.
+
+## 1.5.0
+
+* `new Chain.parse()` now parses all the stack trace formats supported by `new
+  Trace.parse()`. Formats other than that emitted by `Chain.toString()` will
+  produce single-element chains.
+
+* `new Trace.parse()` now parses the output of `Chain.toString()`. It produces
+  the same result as `Chain.parse().toTrace()`.
+
+## 1.4.2
+
+* Improve the display of `data:` URIs in stack traces.
+
+## 1.4.1
+
+* Fix a crashing bug in `UnparsedFrame.toString()`.
+
+## 1.4.0
+
+* `new Trace.parse()` and related constructors will no longer throw an exception
+  if they encounter an unparseable stack frame. Instead, they will generate an
+  `UnparsedFrame`, which exposes no metadata but preserves the frame's original
+  text.
+
+* Properly parse native-code V8 frames.
+
+## 1.3.5
+
+* Properly shorten library names for pathnames of folded frames on Windows.
+
+## 1.3.4
+
+* No longer say that stack chains aren't supported on dart2js now that
+  [sdk#15171][] is fixed. Note that this fix only applies to Dart 1.12.
+
+[sdk#15171]: https://github.com/dart-lang/sdk/issues/15171
+
+## 1.3.3
+
+* When a `null` stack trace is passed to a completer or stream controller in
+  nested `Chain.capture()` blocks, substitute the inner block's chain rather
+  than the outer block's.
+
+* Add support for empty chains and chains of empty traces to `Chain.parse()`.
+
+* Don't crash when parsing stack traces from Dart VM stack overflows.
+
+## 1.3.2
+
+* Don't crash when running `Trace.terse` on empty stack traces.
+
+## 1.3.1
+
+* Support more types of JavaScriptCore stack frames.
+
+## 1.3.0
+
+* Support stack traces generated by JavaScriptCore. They can be explicitly
+  parsed via `new Trace.parseJSCore` and `new Frame.parseJSCore`.
+
+## 1.2.4
+
+* Fix a type annotation in `LazyTrace`.
+
+## 1.2.3
+
+* Fix a crash in `Chain.parse`.
+
+## 1.2.2
+
+* Don't print the first folded frame of terse stack traces. This frame
+  is always just an internal isolate message handler anyway. This
+  improves the readability of stack traces, especially in stack chains.
+
+* Remove the line numbers and specific files in all terse folded frames, not
+  just those from core libraries.
+
+* Make padding consistent across all stack traces for `Chain.toString()`.
+
+## 1.2.1
+
+* Add `terse` to `LazyTrace.foldFrames()`.
+
+* Further improve stack chains when using the VM's async/await implementation.
+
+## 1.2.0
+
+* Add a `terse` argument to `Trace.foldFrames()` and `Chain.foldFrames()`. This
+  allows them to inherit the behavior of `Trace.terse` and `Chain.terse` without
+  having to duplicate the logic.
+
+## 1.1.3
+
+* Produce nicer-looking stack chains when using the VM's async/await
+  implementation.
+
+## 1.1.2
+
+* Support VM frames without line *or* column numbers, which async/await programs
+  occasionally generate.
+
+* Replace `<<anonymous closure>_async_body>` in VM frames' members with the
+  terser `<async>`.
+
+## 1.1.1
+
+* Widen the SDK constraint to include 1.7.0-dev.4.0.
+
+## 1.1.0
+
+* Unify the parsing of Safari and Firefox stack traces. This fixes an error in
+  Firefox trace parsing.
+
+* Deprecate `Trace.parseSafari6_0`, `Trace.parseSafari6_1`,
+  `Frame.parseSafari6_0`, and `Frame.parseSafari6_1`.
+
+* Add `Frame.parseSafari`.
+
+## 1.0.3
+
+* Use `Zone.errorCallback` to attach stack chains to all errors without the need
+  for `Chain.track`, which is now deprecated.
+
+## 1.0.2
+
+* Remove a workaround for [issue 17083][].
+
+[issue 17083]: https://github.com/dart-lang/sdk/issues/17083
+
+## 1.0.1
+
+* Synchronous errors in the [Chain.capture] callback are now handled correctly.
+
+## 1.0.0
+
+* No API changes, just declared stable.
+
+## 0.9.3+2
+
+* Update the dependency on path.
+
+* Improve the formatting of library URIs in stack traces.
+
+## 0.9.3+1
+
+* If an error is thrown in `Chain.capture`'s `onError` handler, that error is
+  handled by the parent zone. This matches the behavior of `runZoned` in
+  `dart:async`.
+
+## 0.9.3
+
+* Add a `Chain.foldFrames` method that parallels `Trace.foldFrames`.
+
+* Record anonymous method frames in IE10 as "<fn>".
diff --git a/pkgs/stack_trace/LICENSE b/pkgs/stack_trace/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/stack_trace/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/stack_trace/README.md b/pkgs/stack_trace/README.md
new file mode 100644
index 0000000..b10a556
--- /dev/null
+++ b/pkgs/stack_trace/README.md
@@ -0,0 +1,169 @@
+[![Build Status](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml)
+[![pub package](https://img.shields.io/pub/v/stack_trace.svg)](https://pub.dev/packages/stack_trace)
+[![package publisher](https://img.shields.io/pub/publisher/stack_trace.svg)](https://pub.dev/packages/stack_trace/publisher)
+
+This library provides the ability to parse, inspect, and manipulate stack traces
+produced by the underlying Dart implementation. It also provides functions to
+produce string representations of stack traces in a more readable format than
+the native [StackTrace] implementation.
+
+`Trace`s can be parsed from native [StackTrace]s using `Trace.from`, or captured
+using `Trace.current`. Native [StackTrace]s can also be directly converted to
+human-readable strings using `Trace.format`.
+
+[StackTrace]: https://api.dart.dev/stable/dart-core/StackTrace-class.html
+
+Here's an example native stack trace from debugging this library:
+
+    #0      Object.noSuchMethod (dart:core-patch:1884:25)
+    #1      Trace.terse.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:47:21)
+    #2      IterableMixinWorkaround.reduce (dart:collection:29:29)
+    #3      List.reduce (dart:core-patch:1247:42)
+    #4      Trace.terse (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:40:35)
+    #5      format (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/stack_trace.dart:24:28)
+    #6      main.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/test.dart:21:29)
+    #7      _CatchErrorFuture._sendError (dart:async:525:24)
+    #8      _FutureImpl._setErrorWithoutAsyncTrace (dart:async:393:26)
+    #9      _FutureImpl._setError (dart:async:378:31)
+    #10     _ThenFuture._sendValue (dart:async:490:16)
+    #11     _FutureImpl._handleValue.<anonymous closure> (dart:async:349:28)
+    #12     Timer.run.<anonymous closure> (dart:async:2402:21)
+    #13     Timer.Timer.<anonymous closure> (dart:async-patch:15:15)
+
+and its human-readable representation:
+
+    dart:core-patch 1884:25                     Object.noSuchMethod
+    pkg/stack_trace/lib/src/trace.dart 47:21    Trace.terse.<fn>
+    dart:collection 29:29                       IterableMixinWorkaround.reduce
+    dart:core-patch 1247:42                     List.reduce
+    pkg/stack_trace/lib/src/trace.dart 40:35    Trace.terse
+    pkg/stack_trace/lib/stack_trace.dart 24:28  format
+    test.dart 21:29                             main.<fn>
+    dart:async 525:24                           _CatchErrorFuture._sendError
+    dart:async 393:26                           _FutureImpl._setErrorWithoutAsyncTrace
+    dart:async 378:31                           _FutureImpl._setError
+    dart:async 490:16                           _ThenFuture._sendValue
+    dart:async 349:28                           _FutureImpl._handleValue.<fn>
+    dart:async 2402:21                          Timer.run.<fn>
+    dart:async-patch 15:15                      Timer.Timer.<fn>
+
+You can further clean up the stack trace using `Trace.terse`. This folds
+together multiple stack frames from the Dart core libraries, so that only the
+core library method that was directly called from user code is visible. For
+example:
+
+    dart:core                                   Object.noSuchMethod
+    pkg/stack_trace/lib/src/trace.dart 47:21    Trace.terse.<fn>
+    dart:core                                   List.reduce
+    pkg/stack_trace/lib/src/trace.dart 40:35    Trace.terse
+    pkg/stack_trace/lib/stack_trace.dart 24:28  format
+    test.dart 21:29                             main.<fn>
+
+## Stack Chains
+
+This library also provides the ability to capture "stack chains" with the
+`Chain` class. When writing asynchronous code, a single stack trace isn't very
+useful, since the call stack is unwound every time something async happens. A
+stack chain tracks stack traces through asynchronous calls, so that you can see
+the full path from `main` down to the error.
+
+To use stack chains, just wrap the code that you want to track in
+`Chain.capture`. This will create a new [Zone][] in which stack traces are
+recorded and woven into chains every time an asynchronous call occurs. Zones are
+sticky, too, so any asynchronous operations started in the `Chain.capture`
+callback will have their chains tracked, as will asynchronous operations they
+start and so on.
+
+Here's an example of some code that doesn't capture its stack chains:
+
+```dart
+import 'dart:async';
+
+void main() {
+  _scheduleAsync();
+}
+
+void _scheduleAsync() {
+  Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+  throw 'oh no!';
+}
+```
+
+If we run this, it prints the following:
+
+    Unhandled exception:
+    oh no!
+    #0      _runAsync (file:///Users/kevmoo/github/stack_trace/example/example.dart:12:3)
+    #1      _scheduleAsync.<anonymous closure> (file:///Users/kevmoo/github/stack_trace/example/example.dart:8:52)
+    <asynchronous suspension>
+
+Notice how there's no mention of `main` in that stack trace. All we know is that
+the error was in `runAsync`; we don't know why `runAsync` was called.
+
+Now let's look at the same code with stack chains captured:
+
+```dart
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+  Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+  Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+  throw 'oh no!';
+}
+```
+
+Now if we run it, it prints this:
+
+    Unhandled exception:
+    oh no!
+    example/example.dart 14:3                                     _runAsync
+    example/example.dart 10:52                                    _scheduleAsync.<fn>
+    package:stack_trace/src/stack_zone_specification.dart 126:26  StackZoneSpecification._registerUnaryCallback.<fn>.<fn>
+    package:stack_trace/src/stack_zone_specification.dart 208:15  StackZoneSpecification._run
+    package:stack_trace/src/stack_zone_specification.dart 126:14  StackZoneSpecification._registerUnaryCallback.<fn>
+    dart:async/zone.dart 1406:47                                  _rootRunUnary
+    dart:async/zone.dart 1307:19                                  _CustomZone.runUnary
+    ===== asynchronous gap ===========================
+    dart:async/zone.dart 1328:19                                  _CustomZone.registerUnaryCallback
+    dart:async/future_impl.dart 315:23                            Future.then
+    example/example.dart 10:40                                    _scheduleAsync
+    package:stack_trace/src/chain.dart 97:24                      Chain.capture.<fn>
+    dart:async/zone.dart 1398:13                                  _rootRun
+    dart:async/zone.dart 1300:19                                  _CustomZone.run
+    dart:async/zone.dart 1803:10                                  _runZoned
+    dart:async/zone.dart 1746:10                                  runZoned
+    package:stack_trace/src/chain.dart 95:12                      Chain.capture
+    example/example.dart 6:9                                      main
+    dart:isolate-patch/isolate_patch.dart 297:19                  _delayEntrypointInvocation.<fn>
+    dart:isolate-patch/isolate_patch.dart 192:12                  _RawReceivePortImpl._handleMessage
+
+That's a lot of text! If you look closely, though, you can see that `main` is
+listed in the first trace in the chain.
+
+Thankfully, you can call `Chain.terse` just like `Trace.terse` to get rid of all
+the frames you don't care about. The terse version of the stack chain above is
+this:
+
+    test.dart 17:3       runAsync
+    test.dart 13:28      scheduleAsync.<fn>
+    ===== asynchronous gap ===========================
+    dart:async           _Future.then
+    test.dart 13:12      scheduleAsync
+    test.dart 7:18       main.<fn>
+    package:stack_trace  Chain.capture
+    test.dart 6:16       main
+
+That's a lot easier to understand!
+
+[Zone]: https://api.dart.dev/stable/dart-async/Zone-class.html
diff --git a/pkgs/stack_trace/analysis_options.yaml b/pkgs/stack_trace/analysis_options.yaml
new file mode 100644
index 0000000..4eb82ce
--- /dev/null
+++ b/pkgs/stack_trace/analysis_options.yaml
@@ -0,0 +1,22 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - avoid_private_typedef_functions
+    - avoid_redundant_argument_values
+    - avoid_unused_constructor_parameters
+    - avoid_void_async
+    - cancel_subscriptions
+    - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - prefer_const_declarations
+    - unnecessary_await_in_return
+    - use_string_buffers
diff --git a/pkgs/stack_trace/example/example.dart b/pkgs/stack_trace/example/example.dart
new file mode 100644
index 0000000..d601ca4
--- /dev/null
+++ b/pkgs/stack_trace/example/example.dart
@@ -0,0 +1,15 @@
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+  Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+  Future<void>.delayed(const Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+  throw StateError('oh no!');
+}
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart
new file mode 100644
index 0000000..6a815c6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -0,0 +1,264 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:math' as math;
+
+import 'frame.dart';
+import 'lazy_chain.dart';
+import 'stack_zone_specification.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A function that handles errors in the zone wrapped by [Chain.capture].
+@Deprecated('Will be removed in stack_trace 2.0.0.')
+typedef ChainHandler = void Function(dynamic error, Chain chain);
+
+/// An opaque key used to track the current [StackZoneSpecification].
+final _specKey = Object();
+
+/// A chain of stack traces.
+///
+/// A stack chain is a collection of one or more stack traces that collectively
+/// represent the path from `main` through nested function calls to a particular
+/// code location, usually where an error was thrown. Multiple stack traces are
+/// necessary when using asynchronous functions, since the program's stack is
+/// reset before each asynchronous callback is run.
+///
+/// Stack chains can be automatically tracked using [Chain.capture]. This sets
+/// up a new [Zone] in which the current stack chain is tracked and can be
+/// accessed using [Chain.current]. Any errors that would be top-leveled in
+/// the zone can be handled, along with their associated chains, with the
+/// `onError` callback. For example:
+///
+///     Chain.capture(() {
+///       // ...
+///     }, onError: (error, stackChain) {
+///       print("Caught error $error\n"
+///             "$stackChain");
+///     });
+class Chain implements StackTrace {
+  /// The stack traces that make up this chain.
+  ///
+  /// Like the frames in a stack trace, the traces are ordered from most local
+  /// to least local. The first one is the trace where the actual exception was
+  /// raised, the second one is where that callback was scheduled, and so on.
+  final List<Trace> traces;
+
+  /// The [StackZoneSpecification] for the current zone.
+  static StackZoneSpecification? get _currentSpec =>
+      Zone.current[_specKey] as StackZoneSpecification?;
+
+  /// If [when] is `true`, runs [callback] in a [Zone] in which the current
+  /// stack chain is tracked and automatically associated with (most) errors.
+  ///
+  /// If [when] is `false`, this does not track stack chains. Instead, it's
+  /// identical to [runZoned], except that it wraps any errors in
+  /// [Chain.forTrace]—which will only wrap the trace unless there's a different
+  /// [Chain.capture] active. This makes it easy for the caller to only capture
+  /// stack chains in debug mode or during development.
+  ///
+  /// If [onError] is passed, any error in the zone that would otherwise go
+  /// unhandled is passed to it, along with the [Chain] associated with that
+  /// error. Note that if [callback] produces multiple unhandled errors,
+  /// [onError] may be called more than once. If [onError] isn't passed, the
+  /// parent Zone's `unhandledErrorHandler` will be called with the error and
+  /// its chain.
+  ///
+  /// The zone this creates will be an error zone if either [onError] is
+  /// not `null` and [when] is false,
+  /// or if both [when] and [errorZone] are `true`.
+  ///  If [errorZone] is `false`, [onError] must be `null`.
+  ///
+  /// If [callback] returns a value, it will be returned by [capture] as well.
+  ///
+  /// [zoneValues] is added to the [runZoned] calls.
+  static T capture<T>(T Function() callback,
+      {void Function(Object error, Chain)? onError,
+      bool when = true,
+      bool errorZone = true,
+      Map<Object?, Object?>? zoneValues}) {
+    if (!errorZone && onError != null) {
+      throw ArgumentError.value(
+          onError, 'onError', 'must be null if errorZone is false');
+    }
+
+    if (!when) {
+      if (onError == null) return runZoned(callback, zoneValues: zoneValues);
+      return runZonedGuarded(callback, (error, stackTrace) {
+        onError(error, Chain.forTrace(stackTrace));
+      }, zoneValues: zoneValues) as T;
+    }
+
+    var spec = StackZoneSpecification(onError, errorZone: errorZone);
+    return runZoned(() {
+      try {
+        return callback();
+      } on Object catch (error, stackTrace) {
+        // Forward synchronous errors through the async error path to match the
+        // behavior of `runZonedGuarded`.
+        Zone.current.handleUncaughtError(error, stackTrace);
+
+        // If the expected return type of capture() is not nullable, this will
+        // throw a cast exception. But the only other alternative is to throw
+        // some other exception. Casting null to T at least lets existing uses
+        // where T is a nullable type continue to work.
+        return null as T;
+      }
+    }, zoneSpecification: spec.toSpec(), zoneValues: {
+      ...?zoneValues,
+      _specKey: spec,
+      StackZoneSpecification.disableKey: false
+    });
+  }
+
+  /// 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 Function() 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
+  /// exceptions reported with [Completer.completeError] and
+  /// [StreamController.addError] were tracked correctly.
+  @Deprecated('Chain.track is not necessary in Dart 1.7+.')
+  static dynamic track(Object? futureOrStream) => futureOrStream;
+
+  /// Returns the current stack chain.
+  ///
+  /// By default, the first frame of the first trace will be the line where
+  /// [Chain.current] is called. If [level] is passed, the first trace will
+  /// start that many frames up instead.
+  ///
+  /// If this is called outside of a [capture] zone, it just returns a
+  /// single-trace chain.
+  factory Chain.current([int level = 0]) {
+    if (_currentSpec != null) return _currentSpec!.currentChain(level + 1);
+
+    var chain = Chain.forTrace(StackTrace.current);
+    return LazyChain(() {
+      // JS includes a frame for the call to StackTrace.current, but the VM
+      // doesn't, so we skip an extra frame in a JS context.
+      var first = Trace(chain.traces.first.frames.skip(level + (inJS ? 2 : 1)),
+          original: chain.traces.first.original.toString());
+      return Chain([first, ...chain.traces.skip(1)]);
+    });
+  }
+
+  /// Returns the stack chain associated with [trace].
+  ///
+  /// The first stack trace in the returned chain will always be [trace]
+  /// (converted to a [Trace] if necessary). If there is no chain associated
+  /// with [trace] or if this is called outside of a [capture] zone, this just
+  /// returns a single-trace chain containing [trace].
+  ///
+  /// If [trace] is already a [Chain], it will be returned as-is.
+  factory Chain.forTrace(StackTrace trace) {
+    if (trace is Chain) return trace;
+    if (_currentSpec != null) return _currentSpec!.chainFor(trace);
+    if (trace is Trace) return Chain([trace]);
+    return LazyChain(() => Chain.parse(trace.toString()));
+  }
+
+  /// Parses a string representation of a stack chain.
+  ///
+  /// If [chain] is the output of a call to [Chain.toString], it will be parsed
+  /// as a full stack chain. Otherwise, it will be parsed as in [Trace.parse]
+  /// and returned as a single-trace chain.
+  factory Chain.parse(String chain) {
+    if (chain.isEmpty) return Chain([]);
+    if (chain.contains(vmChainGap)) {
+      return Chain(chain
+          .split(vmChainGap)
+          .where((line) => line.isNotEmpty)
+          .map(Trace.parseVM));
+    }
+    if (!chain.contains(chainGap)) return Chain([Trace.parse(chain)]);
+
+    return Chain(chain.split(chainGap).map(Trace.parseFriendly));
+  }
+
+  /// Returns a new [Chain] comprised of [traces].
+  Chain(Iterable<Trace> traces) : traces = List<Trace>.unmodifiable(traces);
+
+  /// Returns a terser version of this chain.
+  ///
+  /// This calls [Trace.terse] on every trace in [traces], and discards any
+  /// trace that contain only internal frames.
+  ///
+  /// This won't do anything with a raw JavaScript trace, since there's no way
+  /// to determine which frames come from which Dart libraries. However, the
+  /// [`source_map_stack_trace`](https://pub.dev/packages/source_map_stack_trace)
+  /// package can be used to convert JavaScript traces into Dart-style traces.
+  Chain get terse => foldFrames((_) => false, terse: true);
+
+  /// Returns a new [Chain] based on this chain where multiple stack frames
+  /// matching [predicate] are folded together.
+  ///
+  /// This means that whenever there are multiple frames in a row that match
+  /// [predicate], only the last one is kept. In addition, traces that are
+  /// composed entirely of frames matching [predicate] are omitted.
+  ///
+  /// This is useful for limiting the amount of library code that appears in a
+  /// stack trace by only showing user code and code that's called by user code.
+  ///
+  /// If [terse] is true, this will also fold together frames from the core
+  /// library or from this package, and simplify core library frames as in
+  /// [Trace.terse].
+  Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+    var foldedTraces =
+        traces.map((trace) => trace.foldFrames(predicate, terse: terse));
+    var nonEmptyTraces = foldedTraces.where((trace) {
+      // Ignore traces that contain only folded frames.
+      if (trace.frames.length > 1) return true;
+      if (trace.frames.isEmpty) return false;
+
+      // In terse mode, the trace may have removed an outer folded frame,
+      // leaving a single non-folded frame. We can detect a folded frame because
+      // it has no line information.
+      if (!terse) return false;
+      return trace.frames.single.line != null;
+    });
+
+    // If all the traces contain only internal processing, preserve the last
+    // (top-most) one so that the chain isn't empty.
+    if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) {
+      return Chain([foldedTraces.last]);
+    }
+
+    return Chain(nonEmptyTraces);
+  }
+
+  /// Converts this chain to a [Trace].
+  ///
+  /// The trace version of a chain is just the concatenation of all the traces
+  /// in the chain.
+  Trace toTrace() => Trace(traces.expand((trace) => trace.frames));
+
+  @override
+  String toString() {
+    // Figure out the longest path so we know how much to pad.
+    var longest = traces
+        .map((trace) => trace.frames
+            .map((frame) => frame.location.length)
+            .fold(0, math.max))
+        .fold(0, math.max);
+
+    // Don't call out to [Trace.toString] here because that doesn't ensure that
+    // padding is consistent across all traces.
+    return traces
+        .map((trace) => trace.frames
+            .map((frame) =>
+                '${frame.location.padRight(longest)}  ${frame.member}\n')
+            .join())
+        .join(chainGap);
+  }
+}
diff --git a/pkgs/stack_trace/lib/src/frame.dart b/pkgs/stack_trace/lib/src/frame.dart
new file mode 100644
index 0000000..d4043b7
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/frame.dart
@@ -0,0 +1,458 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+
+import 'trace.dart';
+import 'unparsed_frame.dart';
+
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart:42)
+// #1      Foo._bar (file:///home/nweiz/code/stuff.dart)
+final _vmFrame = RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$');
+
+//     at Object.stringify (native)
+//     at VW.call$0 (https://example.com/stuff.dart.js:560:28)
+//     at VW.call$0 (eval as fn
+//         (https://example.com/stuff.dart.js:560:28), efn:3:28)
+//     at https://example.com/stuff.dart.js:560:28
+final _v8JsFrame =
+    RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$');
+
+// https://example.com/stuff.dart.js:560:28
+// https://example.com/stuff.dart.js:560
+//
+// Group 1: URI, required
+// Group 2: line number, required
+// Group 3: column number, optional
+final _v8JsUrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$');
+
+// With names:
+//
+//     at Error.f (wasm://wasm/0006d966:wasm-function[119]:0xbb13)
+//     at g (wasm://wasm/0006d966:wasm-function[796]:0x143b4)
+//
+// Without names:
+//
+//     at wasm://wasm/0005168a:wasm-function[119]:0xbb13
+//     at wasm://wasm/0005168a:wasm-function[796]:0x143b4
+//
+// Matches named groups:
+//
+// - "member": optional, `Error.f` in the first example, NA in the second.
+// - "uri":  `wasm://wasm/0006d966`.
+// - "index": `119`.
+// - "offset": (hex number) `bb13`.
+//
+// To avoid having multiple groups for the same part of the frame, this regex
+// matches unmatched parentheses after the member name.
+final _v8WasmFrame = RegExp(r'^\s*at (?:(?<member>.+) )?'
+    r'(?:\(?(?:(?<uri>\S+):wasm-function\[(?<index>\d+)\]'
+    r'\:0x(?<offset>[0-9a-fA-F]+))\)?)$');
+
+// eval as function (https://example.com/stuff.dart.js:560:28), efn:3:28
+// eval as function (https://example.com/stuff.dart.js:560:28)
+// eval as function (eval as otherFunction
+//     (https://example.com/stuff.dart.js:560:28))
+final _v8EvalLocation =
+    RegExp(r'^eval at (?:\S.*?) \((.*)\)(?:, .*?:\d+:\d+)?$');
+
+// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+final _firefoxEvalLocation =
+    RegExp(r'(\S+)@(\S+) line (\d+) >.* (Function|eval):\d+:\d+');
+
+// .VW.call$0@https://example.com/stuff.dart.js:560
+// .VW.call$0("arg")@https://example.com/stuff.dart.js:560
+// .VW.call$0/name<@https://example.com/stuff.dart.js:560
+// .VW.call$0@https://example.com/stuff.dart.js:560:36
+// https://example.com/stuff.dart.js:560
+final _firefoxSafariJSFrame = RegExp(r'^'
+    r'(?:' // Member description. Not present in some Safari frames.
+    r'([^@(/]*)' // The actual name of the member.
+    r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox.
+    r'((?:/[^/]*)*)' // Extra characters indicating a nested closure.
+    r'(?:\(.*\))?' // Arguments to the closure.
+    r'@'
+    r')?'
+    r'(.*?)' // The frame's URL.
+    r':'
+    r'(\d*)' // The line number. Empty in Safari if it's unknown.
+    r'(?::(\d*))?' // The column number. Not present in older browsers and
+    // empty in Safari if it's unknown.
+    r'$');
+
+// With names:
+//
+// g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// f@http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// main@http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// Without names:
+//
+// @http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// @http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// @http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// JSShell in the command line uses a different format, which this regex also
+// parses.
+//
+// With names:
+//
+// main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Without names:
+//
+// @/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Matches named groups:
+//
+// - "member": Function name, may be empty: `g`.
+// - "uri": `http://localhost:8080/test.wasm`.
+// - "index": `796`.
+// - "offset": (in hex) `143b4`.
+final _firefoxWasmFrame =
+    RegExp(r'^(?<member>.*?)@(?:(?<uri>\S+).*?:wasm-function'
+        r'\[(?<index>\d+)\]:0x(?<offset>[0-9a-fA-F]+))$');
+
+// With names:
+//
+// (Note: Lines below are literal text, e.g. <?> is not a placeholder, it's a
+// part of the stack frame.)
+//
+// <?>.wasm-function[g]@[wasm code]
+// <?>.wasm-function[f]@[wasm code]
+// <?>.wasm-function[main]@[wasm code]
+//
+// Without names:
+//
+// <?>.wasm-function[796]@[wasm code]
+// <?>.wasm-function[795]@[wasm code]
+// <?>.wasm-function[792]@[wasm code]
+//
+// Matches named group "member": `g` or `796`.
+final _safariWasmFrame =
+    RegExp(r'^.*?wasm-function\[(?<member>.*)\]@\[wasm code\]$');
+
+// foo/bar.dart 10:11 Foo._bar
+// foo/bar.dart 10:11 (anonymous function).dart.fn
+// https://dart.dev/foo/bar.dart Foo._bar
+// data:... 10:11 Foo._bar
+final _friendlyFrame = RegExp(r'^(\S+)(?: (\d+)(?::(\d+))?)?\s+([^\d].*)$');
+
+/// A regular expression that matches asynchronous member names generated by the
+/// VM.
+final _asyncBody = RegExp(r'<(<anonymous closure>|[^>]+)_async_body>');
+
+final _initialDot = RegExp(r'^\.');
+
+/// A single stack frame. Each frame points to a precise location in Dart code.
+class Frame {
+  /// The URI of the file in which the code is located.
+  ///
+  /// This URI will usually have the scheme `dart`, `file`, `http`, or `https`.
+  final Uri uri;
+
+  /// The line number on which the code location is located.
+  ///
+  /// This can be null, indicating that the line number is unknown or
+  /// unimportant.
+  final int? line;
+
+  /// The column number of the code location.
+  ///
+  /// This can be null, indicating that the column number is unknown or
+  /// unimportant.
+  final int? column;
+
+  /// The name of the member in which the code location occurs.
+  ///
+  /// Anonymous closures are represented as `<fn>` in this member string.
+  final String? member;
+
+  /// Whether this stack frame comes from the Dart core libraries.
+  bool get isCore => uri.scheme == 'dart';
+
+  /// Returns a human-friendly description of the library that this stack frame
+  /// comes from.
+  ///
+  /// This will usually be the string form of [uri], but a relative URI will be
+  /// used if possible. Data URIs will be truncated.
+  String get library {
+    if (uri.scheme == 'data') return 'data:...';
+    return path.prettyUri(uri);
+  }
+
+  /// Returns the name of the package this stack frame comes from, or `null` if
+  /// this stack frame doesn't come from a `package:` URL.
+  String? get package {
+    if (uri.scheme != 'package') return null;
+    return uri.path.split('/').first;
+  }
+
+  /// A human-friendly description of the code location.
+  String get location {
+    if (line == null) return library;
+    if (column == null) return '$library $line';
+    return '$library $line:$column';
+  }
+
+  /// Returns a single frame of the current stack.
+  ///
+  /// By default, this will return the frame above the current method. If
+  /// [level] is `0`, it will return the current method's frame; if [level] is
+  /// higher than `1`, it will return higher frames.
+  factory Frame.caller([int level = 1]) {
+    if (level < 0) {
+      throw ArgumentError('Argument [level] must be greater than or equal '
+          'to 0.');
+    }
+
+    return Trace.current(level + 1).frames.first;
+  }
+
+  /// Parses a string representation of a Dart VM stack frame.
+  factory Frame.parseVM(String frame) => _catchFormatException(frame, () {
+        // The VM sometimes folds multiple stack frames together and replaces
+        // them with "...".
+        if (frame == '...') {
+          return Frame(Uri(), null, null, '...');
+        }
+
+        var match = _vmFrame.firstMatch(frame);
+        if (match == null) return UnparsedFrame(frame);
+
+        // Get the pieces out of the regexp match. Function, URI and line should
+        // always be found. The column is optional.
+        var member = match[1]!
+            .replaceAll(_asyncBody, '<async>')
+            .replaceAll('<anonymous closure>', '<fn>');
+        var uri = match[2]!.startsWith('<data:')
+            ? Uri.dataFromString('')
+            : Uri.parse(match[2]!);
+
+        var lineAndColumn = match[3]!.split(':');
+        var line =
+            lineAndColumn.length > 1 ? int.parse(lineAndColumn[1]) : null;
+        var column =
+            lineAndColumn.length > 2 ? int.parse(lineAndColumn[2]) : null;
+        return Frame(uri, line, column, member);
+      });
+
+  /// Parses a string representation of a Chrome/V8 stack frame.
+  factory Frame.parseV8(String frame) => _catchFormatException(frame, () {
+        // Try to match a Wasm frame first: the Wasm frame regex won't match a
+        // JS frame but the JS frame regex may match a Wasm frame.
+        var match = _v8WasmFrame.firstMatch(frame);
+        if (match != null) {
+          final member = match.namedGroup('member');
+          final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+          final functionIndex = match.namedGroup('index')!;
+          final functionOffset =
+              int.parse(match.namedGroup('offset')!, radix: 16);
+          return Frame(uri, 1, functionOffset + 1, member ?? functionIndex);
+        }
+
+        match = _v8JsFrame.firstMatch(frame);
+        if (match != null) {
+          // v8 location strings can be arbitrarily-nested, since it adds a
+          // layer of nesting for each eval performed on that line.
+          Frame parseJsLocation(String location, String member) {
+            var evalMatch = _v8EvalLocation.firstMatch(location);
+            while (evalMatch != null) {
+              location = evalMatch[1]!;
+              evalMatch = _v8EvalLocation.firstMatch(location);
+            }
+
+            if (location == 'native') {
+              return Frame(Uri.parse('native'), null, null, member);
+            }
+
+            var urlMatch = _v8JsUrlLocation.firstMatch(location);
+            if (urlMatch == null) return UnparsedFrame(frame);
+
+            final uri = _uriOrPathToUri(urlMatch[1]!);
+            final line = int.parse(urlMatch[2]!);
+            final columnMatch = urlMatch[3];
+            final column = columnMatch != null ? int.parse(columnMatch) : null;
+            return Frame(uri, line, column, member);
+          }
+
+          // V8 stack frames can be in two forms.
+          if (match[2] != null) {
+            // The first form looks like " at FUNCTION (LOCATION)". V8 proper
+            // lists anonymous functions within eval as "<anonymous>", while
+            // IE10 lists them as "Anonymous function".
+            return parseJsLocation(
+                match[2]!,
+                match[1]!
+                    .replaceAll('<anonymous>', '<fn>')
+                    .replaceAll('Anonymous function', '<fn>')
+                    .replaceAll('(anonymous function)', '<fn>'));
+          } else {
+            // The second form looks like " at LOCATION", and is used for
+            // anonymous functions.
+            return parseJsLocation(match[3]!, '<fn>');
+          }
+        }
+
+        return UnparsedFrame(frame);
+      });
+
+  /// Parses a string representation of a JavaScriptCore stack trace.
+  factory Frame.parseJSCore(String frame) => Frame.parseV8(frame);
+
+  /// Parses a string representation of an IE stack frame.
+  ///
+  /// IE10+ frames look just like V8 frames. Prior to IE10, stack traces can't
+  /// be retrieved.
+  factory Frame.parseIE(String frame) => Frame.parseV8(frame);
+
+  /// Parses a Firefox 'eval' or 'function' stack frame.
+  ///
+  /// For example:
+  ///
+  /// ```
+  /// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+  /// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+  /// ```
+  factory Frame._parseFirefoxEval(String frame) =>
+      _catchFormatException(frame, () {
+        final match = _firefoxEvalLocation.firstMatch(frame);
+        if (match == null) return UnparsedFrame(frame);
+        var member = match[1]!.replaceAll('/<', '');
+        final uri = _uriOrPathToUri(match[2]!);
+        final line = int.parse(match[3]!);
+        if (member.isEmpty || member == 'anonymous') {
+          member = '<fn>';
+        }
+        return Frame(uri, line, null, member);
+      });
+
+  /// Parses a string representation of a Firefox or Safari stack frame.
+  factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () {
+        var match = _firefoxSafariJSFrame.firstMatch(frame);
+        if (match != null) {
+          if (match[3]!.contains(' line ')) {
+            return Frame._parseFirefoxEval(frame);
+          }
+
+          // Normally this is a URI, but in a jsshell trace it can be a path.
+          var uri = _uriOrPathToUri(match[3]!);
+
+          var member = match[1];
+          if (member != null) {
+            member +=
+                List.filled('/'.allMatches(match[2]!).length, '.<fn>').join();
+            if (member == '') member = '<fn>';
+
+            // Some Firefox members have initial dots. We remove them for
+            // consistency with other platforms.
+            member = member.replaceFirst(_initialDot, '');
+          } else {
+            member = '<fn>';
+          }
+
+          var line = match[4] == '' ? null : int.parse(match[4]!);
+          var column =
+              match[5] == null || match[5] == '' ? null : int.parse(match[5]!);
+          return Frame(uri, line, column, member);
+        }
+
+        match = _firefoxWasmFrame.firstMatch(frame);
+        if (match != null) {
+          final member = match.namedGroup('member')!;
+          final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+          final functionIndex = match.namedGroup('index')!;
+          final functionOffset =
+              int.parse(match.namedGroup('offset')!, radix: 16);
+          return Frame(uri, 1, functionOffset + 1,
+              member.isNotEmpty ? member : functionIndex);
+        }
+
+        match = _safariWasmFrame.firstMatch(frame);
+        if (match != null) {
+          final member = match.namedGroup('member')!;
+          return Frame(Uri(path: 'wasm code'), null, null, member);
+        }
+
+        return UnparsedFrame(frame);
+      });
+
+  /// Parses a string representation of a Safari 6.0 stack frame.
+  @Deprecated('Use Frame.parseSafari instead.')
+  factory Frame.parseSafari6_0(String frame) => Frame.parseFirefox(frame);
+
+  /// Parses a string representation of a Safari 6.1+ stack frame.
+  @Deprecated('Use Frame.parseSafari instead.')
+  factory Frame.parseSafari6_1(String frame) => Frame.parseFirefox(frame);
+
+  /// Parses a string representation of a Safari stack frame.
+  factory Frame.parseSafari(String frame) => Frame.parseFirefox(frame);
+
+  /// Parses this package's string representation of a stack frame.
+  factory Frame.parseFriendly(String frame) => _catchFormatException(frame, () {
+        var match = _friendlyFrame.firstMatch(frame);
+        if (match == null) {
+          throw FormatException(
+              "Couldn't parse package:stack_trace stack trace line '$frame'.");
+        }
+        // Fake truncated data urls generated by the friendly stack trace format
+        // cause Uri.parse to throw an exception so we have to special case
+        // them.
+        var uri = match[1] == 'data:...'
+            ? Uri.dataFromString('')
+            : Uri.parse(match[1]!);
+        // If there's no scheme, this is a relative URI. We should interpret it
+        // as relative to the current working directory.
+        if (uri.scheme == '') {
+          uri = path.toUri(path.absolute(path.fromUri(uri)));
+        }
+
+        var line = match[2] == null ? null : int.parse(match[2]!);
+        var column = match[3] == null ? null : int.parse(match[3]!);
+        return Frame(uri, line, column, match[4]);
+      });
+
+  /// A regular expression matching an absolute URI.
+  static final _uriRegExp = RegExp(r'^[a-zA-Z][-+.a-zA-Z\d]*://');
+
+  /// A regular expression matching a Windows path.
+  static final _windowsRegExp = RegExp(r'^([a-zA-Z]:[\\/]|\\\\)');
+
+  /// Converts [uriOrPath], which can be a URI, a Windows path, or a Posix path,
+  /// to a URI (absolute if possible).
+  static Uri _uriOrPathToUri(String uriOrPath) {
+    if (uriOrPath.contains(_uriRegExp)) {
+      return Uri.parse(uriOrPath);
+    } else if (uriOrPath.contains(_windowsRegExp)) {
+      return Uri.file(uriOrPath, windows: true);
+    } else if (uriOrPath.startsWith('/')) {
+      return Uri.file(uriOrPath, windows: false);
+    }
+
+    // As far as I've seen, Firefox and V8 both always report absolute paths in
+    // their stack frames. However, if we do get a relative path, we should
+    // handle it gracefully.
+    if (uriOrPath.contains('\\')) return path.windows.toUri(uriOrPath);
+    return Uri.parse(uriOrPath);
+  }
+
+  /// Runs [body] and returns its result.
+  ///
+  /// If [body] throws a [FormatException], returns an [UnparsedFrame] with
+  /// [text] instead.
+  static Frame _catchFormatException(String text, Frame Function() body) {
+    try {
+      return body();
+    } on FormatException catch (_) {
+      return UnparsedFrame(text);
+    }
+  }
+
+  Frame(this.uri, this.line, this.column, this.member);
+
+  @override
+  String toString() => '$location in $member';
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_chain.dart b/pkgs/stack_trace/lib/src/lazy_chain.dart
new file mode 100644
index 0000000..063ed59
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_chain.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Chain].
+typedef ChainThunk = Chain Function();
+
+/// A wrapper around a [ChainThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyChain implements Chain {
+  final ChainThunk _thunk;
+  late final Chain _chain = _thunk();
+
+  LazyChain(this._thunk);
+
+  @override
+  List<Trace> get traces => _chain.traces;
+  @override
+  Chain get terse => _chain.terse;
+  @override
+  Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+      LazyChain(() => _chain.foldFrames(predicate, terse: terse));
+  @override
+  Trace toTrace() => LazyTrace(_chain.toTrace);
+  @override
+  String toString() => _chain.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_trace.dart b/pkgs/stack_trace/lib/src/lazy_trace.dart
new file mode 100644
index 0000000..3ecaa2d
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_trace.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'frame.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Trace].
+typedef TraceThunk = Trace Function();
+
+/// A wrapper around a [TraceThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyTrace implements Trace {
+  final TraceThunk _thunk;
+  late final Trace _trace = _thunk();
+
+  LazyTrace(this._thunk);
+
+  @override
+  List<Frame> get frames => _trace.frames;
+  @override
+  StackTrace get original => _trace.original;
+  @override
+  StackTrace get vmTrace => _trace.vmTrace;
+  @override
+  Trace get terse => LazyTrace(() => _trace.terse);
+  @override
+  Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+      LazyTrace(() => _trace.foldFrames(predicate, terse: terse));
+  @override
+  String toString() => _trace.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/stack_zone_specification.dart b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
new file mode 100644
index 0000000..901a5ee
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
@@ -0,0 +1,262 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'chain.dart';
+import 'lazy_chain.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A class encapsulating the zone specification for a [Chain.capture] zone.
+///
+/// Until they're materialized and exposed to the user, stack chains are tracked
+/// as linked lists of [Trace]s using the [_Node] class. These nodes are stored
+/// in three distinct ways:
+///
+/// * When a callback is registered, a node is created and stored as a captured
+///   local variable until the callback is run.
+///
+/// * When a callback is run, its captured node is set as the [_currentNode] so
+///   it can be available to [Chain.current] and to be linked into additional
+///   chains when more callbacks are scheduled.
+///
+/// * When a callback throws an error or a Future or Stream emits an error, the
+///   current node is associated with that error's stack trace using the
+///   [_chains] expando.
+///
+/// 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 = 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
+  /// because it's a common practice to throw strings as errors, which can't be
+  /// used with expandos.
+  ///
+  /// The chain associated with a given stack trace doesn't contain a node for
+  /// that stack trace.
+  final _chains = Expando<_Node>('stack chains');
+
+  /// The error handler for the zone.
+  ///
+  /// If this is null, that indicates that any unhandled errors should be passed
+  /// to the parent zone.
+  final void Function(Object error, Chain)? _onError;
+
+  /// The most recent node of the current stack chain.
+  _Node? _currentNode;
+
+  /// Whether this is an error zone.
+  final bool _errorZone;
+
+  StackZoneSpecification(this._onError, {bool errorZone = true})
+      : _errorZone = errorZone;
+
+  /// Converts this specification to a real [ZoneSpecification].
+  ZoneSpecification toSpec() => ZoneSpecification(
+      handleUncaughtError: _errorZone ? _handleUncaughtError : null,
+      registerCallback: _registerCallback,
+      registerUnaryCallback: _registerUnaryCallback,
+      registerBinaryCallback: _registerBinaryCallback,
+      errorCallback: _errorCallback);
+
+  /// Returns the current stack chain.
+  ///
+  /// By default, the first frame of the first trace will be the line where
+  /// [currentChain] is called. If [level] is passed, the first trace will start
+  /// that many frames up instead.
+  Chain currentChain([int level = 0]) => _createNode(level + 1).toChain();
+
+  /// Returns the stack chain associated with [trace], if one exists.
+  ///
+  /// The first stack trace in the returned chain will always be [trace]
+  /// (converted to a [Trace] if necessary). If there is no chain associated
+  /// with [trace], this just returns a single-trace chain containing [trace].
+  Chain chainFor(StackTrace? trace) {
+    if (trace is Chain) return trace;
+    trace ??= StackTrace.current;
+
+    var previous = _chains[trace] ?? _currentNode;
+    if (previous == null) {
+      // If there's no [_currentNode], we're running synchronously beneath
+      // [Chain.capture] and we should fall back to the VM's stack chaining. We
+      // can't use [Chain.from] here because it'll just call [chainFor] again.
+      if (trace is Trace) return Chain([trace]);
+      return LazyChain(() => Chain.parse(trace!.toString()));
+    } else {
+      if (trace is! Trace) {
+        var original = trace;
+        trace = LazyTrace(() => Trace.parse(_trimVMChain(original)));
+      }
+
+      return _Node(trace, previous).toChain();
+    }
+  }
+
+  /// Tracks the current stack chain so it can be set to [_currentNode] when
+  /// [f] is run.
+  ZoneCallback<R> _registerCallback<R>(
+      Zone self, ZoneDelegate parent, Zone zone, R Function() f) {
+    if (_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 [_currentNode] when
+  /// [f] is run.
+  ZoneUnaryCallback<R, T> _registerUnaryCallback<R, T>(
+      Zone self,
+      ZoneDelegate parent,
+      Zone zone,
+      @pragma('vm:awaiter-link') R Function(T) f) {
+    if (_disabled) return parent.registerUnaryCallback(zone, f);
+    var node = _createNode(1);
+    return parent.registerUnaryCallback(
+        zone, (arg) => _run(() => f(arg), node));
+  }
+
+  /// Tracks the current stack chain so it can be set to [_currentNode] when
+  /// [f] is run.
+  ZoneBinaryCallback<R, T1, T2> _registerBinaryCallback<R, T1, T2>(
+      Zone self, ZoneDelegate parent, Zone zone, R Function(T1, T2) f) {
+    if (_disabled) return parent.registerBinaryCallback(zone, f);
+
+    var node = _createNode(1);
+    return parent.registerBinaryCallback(
+        zone, (arg1, arg2) => _run(() => f(arg1, arg2), node));
+  }
+
+  /// Looks up the chain associated with [stackTrace] and passes it either to
+  /// [_onError] or [parent]'s error handler.
+  void _handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone,
+      Object error, StackTrace stackTrace) {
+    if (_disabled) {
+      parent.handleUncaughtError(zone, error, stackTrace);
+      return;
+    }
+
+    var stackChain = chainFor(stackTrace);
+    if (_onError == null) {
+      parent.handleUncaughtError(zone, error, stackChain);
+      return;
+    }
+
+    // TODO(nweiz): Currently this copies a lot of logic from [runZoned]. Just
+    // allow [runBinary] to throw instead once issue 18134 is fixed.
+    try {
+      // TODO(rnystrom): Is the null-assertion correct here? It is nullable in
+      // Zone. Should we check for that here?
+      self.parent!.runBinary(_onError, error, stackChain);
+    } on Object catch (newError, newStackTrace) {
+      if (identical(newError, error)) {
+        parent.handleUncaughtError(zone, error, stackChain);
+      } else {
+        parent.handleUncaughtError(zone, newError, newStackTrace);
+      }
+    }
+  }
+
+  /// Attaches the current stack chain to [stackTrace], replacing it if
+  /// necessary.
+  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();
+    } else {
+      if (_chains[stackTrace] == null) _chains[stackTrace] = _createNode(2);
+    }
+
+    var asyncError = parent.errorCallback(zone, error, stackTrace);
+    return asyncError ?? AsyncError(error, stackTrace);
+  }
+
+  /// Creates a [_Node] with the current stack trace and linked to
+  /// [_currentNode].
+  ///
+  /// By default, the first frame of the first trace will be the line where
+  /// [_createNode] is called. If [level] is passed, the first trace will start
+  /// that many frames up instead.
+  _Node _createNode([int level = 0]) =>
+      _Node(_currentTrace(level + 1), _currentNode);
+
+  // TODO(nweiz): use a more robust way of detecting and tracking errors when
+  // issue 15105 is fixed.
+  /// Runs [f] with [_currentNode] set to [node].
+  ///
+  /// If [f] throws an error, this associates [node] with that error's stack
+  /// trace.
+  T _run<T>(T Function() f, _Node node) {
+    var previousNode = _currentNode;
+    _currentNode = node;
+    try {
+      return f();
+    } catch (e, stackTrace) {
+      // We can see the same stack trace multiple times if it's rethrown through
+      // guarded callbacks.  The innermost chain will have the most
+      // information so it should take precedence.
+      _chains[stackTrace] ??= node;
+      rethrow;
+    } finally {
+      _currentNode = previousNode;
+    }
+  }
+
+  /// Like [Trace.current], but if the current stack trace has VM chaining
+  /// enabled, this only returns the innermost sub-trace.
+  Trace _currentTrace([int? level]) {
+    var stackTrace = StackTrace.current;
+    return LazyTrace(() {
+      var text = _trimVMChain(stackTrace);
+      var trace = Trace.parse(text);
+      // JS includes a frame for the call to StackTrace.current, but the VM
+      // doesn't, so we skip an extra frame in a JS context.
+      return Trace(trace.frames.skip((level ?? 0) + (inJS ? 2 : 1)),
+          original: text);
+    });
+  }
+
+  /// Removes the VM's stack chains from the native [trace], since we're
+  /// generating our own and we don't want duplicate frames.
+  String _trimVMChain(StackTrace trace) {
+    var text = trace.toString();
+    var index = text.indexOf(vmChainGap);
+    return index == -1 ? text : text.substring(0, index);
+  }
+}
+
+/// A linked list node representing a single entry in a stack chain.
+class _Node {
+  /// The stack trace for this link of the chain.
+  final Trace trace;
+
+  /// The previous node in the chain.
+  final _Node? previous;
+
+  _Node(StackTrace trace, [this.previous]) : trace = Trace.from(trace);
+
+  /// Converts this to a [Chain].
+  Chain toChain() {
+    var nodes = <Trace>[];
+    _Node? node = this;
+    while (node != null) {
+      nodes.add(node.trace);
+      node = node.previous;
+    }
+    return Chain(nodes);
+  }
+}
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
new file mode 100644
index 0000000..b8c62f5
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -0,0 +1,341 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'unparsed_frame.dart';
+import 'utils.dart';
+import 'vm_trace.dart';
+
+final _terseRegExp = RegExp(r'(-patch)?([/\\].*)?$');
+
+/// A RegExp to match V8's stack traces.
+///
+/// V8's traces start with a line that's either just "Error" or else is a
+/// description of the exception that occurred. That description can be multiple
+/// lines, so we just look for any line other than the first that begins with
+/// three or four spaces and "at".
+final _v8Trace = RegExp(r'\n    ?at ');
+
+/// A RegExp to match indidual lines of V8's stack traces.
+///
+/// This is intended to filter out the leading exception details of the trace
+/// though it is possible for the message to match this as well.
+final _v8TraceLine = RegExp(r'    ?at ');
+
+/// A RegExp to match Firefox's eval and Function stack traces.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack
+///
+/// These stack traces look like:
+///
+/// ````
+/// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+/// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+/// ````
+final _firefoxEvalTrace = RegExp(r'@\S+ line \d+ >.* (Function|eval):\d+:\d+');
+
+/// A RegExp to match Firefox and Safari's stack traces.
+///
+/// Firefox and Safari have very similar stack trace formats, so we use the same
+/// logic for parsing them.
+///
+/// Firefox's trace frames start with the name of the function in which the
+/// error occurred, possibly including its parameters inside `()`. For example,
+/// `.VW.call$0("arg")@https://example.com/stuff.dart.js:560`.
+///
+/// Safari traces occasionally don't include the initial method name followed by
+/// "@", and they always have both the line and column number (or just a
+/// trailing colon if no column number is available). They can also contain
+/// empty lines or lines consisting only of `[native code]`.
+final _firefoxSafariTrace = RegExp(
+    r'^'
+    r'(' // Member description. Not present in some Safari frames.
+    r'([.0-9A-Za-z_$/<]|\(.*\))*' // Member name and arguments.
+    r'@'
+    r')?'
+    r'[^\s]*' // Frame URL.
+    r':\d*' // Line or column number. Some older frames only have a line number.
+    r'$',
+    multiLine: true);
+
+/// A RegExp to match this package's stack traces.
+final _friendlyTrace =
+    RegExp(r'^[^\s<][^\s]*( \d+(:\d+)?)?[ \t]+[^\s]+$', multiLine: true);
+
+/// A stack trace, comprised of a list of stack frames.
+class Trace implements StackTrace {
+  /// The stack frames that comprise this stack trace.
+  final List<Frame> frames;
+
+  /// The original stack trace from which this trace was parsed.
+  final StackTrace original;
+
+  /// Returns a human-readable representation of [stackTrace]. If [terse] is
+  /// set, this folds together multiple stack frames from the Dart core
+  /// libraries, so that only the core library method directly called from user
+  /// code is visible (see [Trace.terse]).
+  static String format(StackTrace stackTrace, {bool terse = true}) {
+    var trace = Trace.from(stackTrace);
+    if (terse) trace = trace.terse;
+    return trace.toString();
+  }
+
+  /// Returns the current stack trace.
+  ///
+  /// By default, the first frame of this trace will be the line where
+  /// [Trace.current] is called. If [level] is passed, the trace will start that
+  /// many frames up instead.
+  factory Trace.current([int level = 0]) {
+    if (level < 0) {
+      throw ArgumentError('Argument [level] must be greater than or equal '
+          'to 0.');
+    }
+
+    var trace = Trace.from(StackTrace.current);
+    return LazyTrace(
+      () =>
+          // JS includes a frame for the call to StackTrace.current, but the VM
+          // doesn't, so we skip an extra frame in a JS context.
+          Trace(trace.frames.skip(level + (inJS ? 2 : 1)),
+              original: trace.original.toString()),
+    );
+  }
+
+  /// Returns a new stack trace containing the same data as [trace].
+  ///
+  /// If [trace] is a native [StackTrace], its data will be parsed out; if it's
+  /// a [Trace], it will be returned as-is.
+  factory Trace.from(StackTrace trace) {
+    if (trace is Trace) return trace;
+    if (trace is Chain) return trace.toTrace();
+    return LazyTrace(() => Trace.parse(trace.toString()));
+  }
+
+  /// Parses a string representation of a stack trace.
+  ///
+  /// [trace] should be formatted in the same way as a Dart VM or browser stack
+  /// trace. If it's formatted as a stack chain, this will return the equivalent
+  /// of [Chain.toTrace].
+  factory Trace.parse(String trace) {
+    try {
+      if (trace.isEmpty) return Trace(<Frame>[]);
+      if (trace.contains(_v8Trace)) return Trace.parseV8(trace);
+      if (trace.contains('\tat ')) return Trace.parseJSCore(trace);
+      if (trace.contains(_firefoxSafariTrace) ||
+          trace.contains(_firefoxEvalTrace)) {
+        return Trace.parseFirefox(trace);
+      }
+      if (trace.contains(chainGap)) return Chain.parse(trace).toTrace();
+      if (trace.contains(_friendlyTrace)) {
+        return Trace.parseFriendly(trace);
+      }
+
+      // Default to parsing the stack trace as a VM trace. This is also hit on
+      // IE and Safari, where the stack trace is just an empty string (issue
+      // 11257).
+      return Trace.parseVM(trace);
+    } on FormatException catch (error) {
+      throw FormatException('${error.message}\nStack trace:\n$trace');
+    }
+  }
+
+  /// Parses a string representation of a Dart VM stack trace.
+  Trace.parseVM(String trace) : this(_parseVM(trace), original: trace);
+
+  static List<Frame> _parseVM(String trace) {
+    // Ignore [vmChainGap]. This matches the behavior of
+    // `Chain.parse().toTrace()`.
+    var lines = trace
+        .trim()
+        .replaceAll(vmChainGap, '')
+        .split('\n')
+        .where((line) => line.isNotEmpty);
+
+    if (lines.isEmpty) {
+      return [];
+    }
+
+    var frames = lines.take(lines.length - 1).map(Frame.parseVM).toList();
+
+    // TODO(nweiz): Remove this when issue 23614 is fixed.
+    if (!lines.last.endsWith('.da')) {
+      frames.add(Frame.parseVM(lines.last));
+    }
+
+    return frames;
+  }
+
+  /// Parses a string representation of a Chrome/V8 stack trace.
+  Trace.parseV8(String trace)
+      : this(
+            trace
+                .split('\n')
+                .skip(1)
+                // It's possible that an Exception's description contains a line
+                // that looks like a V8 trace line, which will screw this up.
+                // Unfortunately, that's impossible to detect.
+                .skipWhile((line) => !line.startsWith(_v8TraceLine))
+                .map(Frame.parseV8),
+            original: trace);
+
+  /// Parses a string representation of a JavaScriptCore stack trace.
+  Trace.parseJSCore(String trace)
+      : this(
+            trace
+                .split('\n')
+                .where((line) => line != '\tat ')
+                .map(Frame.parseV8),
+            original: trace);
+
+  /// Parses a string representation of an Internet Explorer stack trace.
+  ///
+  /// IE10+ traces look just like V8 traces. Prior to IE10, stack traces can't
+  /// be retrieved.
+  Trace.parseIE(String trace) : this.parseV8(trace);
+
+  /// Parses a string representation of a Firefox stack trace.
+  Trace.parseFirefox(String trace)
+      : this(
+            trace
+                .trim()
+                .split('\n')
+                .where((line) => line.isNotEmpty && line != '[native code]')
+                .map(Frame.parseFirefox),
+            original: trace);
+
+  /// Parses a string representation of a Safari stack trace.
+  Trace.parseSafari(String trace) : this.parseFirefox(trace);
+
+  /// Parses a string representation of a Safari 6.1+ stack trace.
+  @Deprecated('Use Trace.parseSafari instead.')
+  Trace.parseSafari6_1(String trace) : this.parseSafari(trace);
+
+  /// Parses a string representation of a Safari 6.0 stack trace.
+  @Deprecated('Use Trace.parseSafari instead.')
+  Trace.parseSafari6_0(String trace)
+      : this(
+            trace
+                .trim()
+                .split('\n')
+                .where((line) => line != '[native code]')
+                .map(Frame.parseFirefox),
+            original: trace);
+
+  /// Parses this package's string representation of a stack trace.
+  ///
+  /// This also parses string representations of [Chain]s. They parse to the
+  /// same trace that [Chain.toTrace] would return.
+  Trace.parseFriendly(String trace)
+      : this(
+            trace.isEmpty
+                ? []
+                : trace
+                    .trim()
+                    .split('\n')
+                    // Filter out asynchronous gaps from [Chain]s.
+                    .where((line) => !line.startsWith('====='))
+                    .map(Frame.parseFriendly),
+            original: trace);
+
+  /// Returns a new [Trace] comprised of [frames].
+  Trace(Iterable<Frame> frames, {String? original})
+      : frames = List<Frame>.unmodifiable(frames),
+        original = StackTrace.fromString(original ?? '');
+
+  /// Returns a VM-style [StackTrace] object.
+  ///
+  /// The return value's [toString] method will always return a string
+  /// representation in the Dart VM's stack trace format, regardless of what
+  /// platform is being used.
+  StackTrace get vmTrace => VMTrace(frames);
+
+  /// Returns a terser version of this trace.
+  ///
+  /// This is accomplished by folding together multiple stack frames from the
+  /// core library or from this package, as in [foldFrames]. Remaining core
+  /// library frames have their libraries, "-patch" suffixes, and line numbers
+  /// removed. If the outermost frame of the stack trace is a core library
+  /// frame, it's removed entirely.
+  ///
+  /// This won't do anything with a raw JavaScript trace, since there's no way
+  /// to determine which frames come from which Dart libraries. However, the
+  /// [`source_map_stack_trace`][https://pub.dev/packages/source_map_stack_trace]
+  /// package can be used to convert JavaScript traces into Dart-style traces.
+  ///
+  /// For custom folding, see [foldFrames].
+  Trace get terse => foldFrames((_) => false, terse: true);
+
+  /// Returns a new [Trace] based on `this` where multiple stack frames matching
+  /// [predicate] are folded together.
+  ///
+  /// This means that whenever there are multiple frames in a row that match
+  /// [predicate], only the last one is kept. This is useful for limiting the
+  /// amount of library code that appears in a stack trace by only showing user
+  /// code and code that's called by user code.
+  ///
+  /// If [terse] is true, this will also fold together frames from the core
+  /// library or from this package, simplify core library frames, and
+  /// potentially remove the outermost frame as in [Trace.terse].
+  Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+    if (terse) {
+      var oldPredicate = predicate;
+      predicate = (frame) {
+        if (oldPredicate(frame)) return true;
+
+        if (frame.isCore) return true;
+        if (frame.package == 'stack_trace') return true;
+
+        // Ignore async stack frames without any line or column information.
+        // These come from the VM's async/await implementation and represent
+        // internal frames. They only ever show up in stack chains and are
+        // always surrounded by other traces that are actually useful, so we can
+        // just get rid of them.
+        // TODO(nweiz): Get rid of this logic some time after issue 22009 is
+        // fixed.
+        if (!frame.member!.contains('<async>')) return false;
+        return frame.line == null;
+      };
+    }
+
+    var newFrames = <Frame>[];
+    for (var frame in frames.reversed) {
+      if (frame is UnparsedFrame || !predicate(frame)) {
+        newFrames.add(frame);
+      } else if (newFrames.isEmpty || !predicate(newFrames.last)) {
+        newFrames.add(Frame(frame.uri, frame.line, frame.column, frame.member));
+      }
+    }
+
+    if (terse) {
+      newFrames = newFrames.map((frame) {
+        if (frame is UnparsedFrame || !predicate(frame)) return frame;
+        var library = frame.library.replaceAll(_terseRegExp, '');
+        return Frame(Uri.parse(library), null, null, frame.member);
+      }).toList();
+
+      if (newFrames.length > 1 && predicate(newFrames.first)) {
+        newFrames.removeAt(0);
+      }
+    }
+
+    return Trace(newFrames.reversed, original: original.toString());
+  }
+
+  @override
+  String toString() {
+    // Figure out the longest path so we know how much to pad.
+    var longest =
+        frames.map((frame) => frame.location.length).fold(0, math.max);
+
+    // Print out the stack trace nicely formatted.
+    return frames.map((frame) {
+      if (frame is UnparsedFrame) return '$frame\n';
+      return '${frame.location.padRight(longest)}  ${frame.member}\n';
+    }).join();
+  }
+}
diff --git a/pkgs/stack_trace/lib/src/unparsed_frame.dart b/pkgs/stack_trace/lib/src/unparsed_frame.dart
new file mode 100644
index 0000000..27e97f6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/unparsed_frame.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'frame.dart';
+
+/// A frame that failed to parse.
+///
+/// The [member] property contains the original frame's contents.
+class UnparsedFrame implements Frame {
+  @override
+  final Uri uri = Uri(path: 'unparsed');
+  @override
+  final int? line = null;
+  @override
+  final int? column = null;
+  @override
+  final bool isCore = false;
+  @override
+  final String library = 'unparsed';
+  @override
+  final String? package = null;
+  @override
+  final String location = 'unparsed';
+
+  @override
+  final String member;
+
+  UnparsedFrame(this.member);
+
+  @override
+  String toString() => member;
+}
diff --git a/pkgs/stack_trace/lib/src/utils.dart b/pkgs/stack_trace/lib/src/utils.dart
new file mode 100644
index 0000000..bd971fe
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/utils.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// The line used in the string representation of stack chains to represent
+/// the gap between traces.
+const chainGap = '===== asynchronous gap ===========================\n';
+
+/// The line used in the string representation of VM stack chains to represent
+/// the gap between traces.
+final vmChainGap = RegExp(r'^<asynchronous suspension>\n?$', multiLine: true);
+
+// TODO(nweiz): When cross-platform imports work, use them to set this.
+/// Whether we're running in a JS context.
+const bool inJS = 0.0 is int;
diff --git a/pkgs/stack_trace/lib/src/vm_trace.dart b/pkgs/stack_trace/lib/src/vm_trace.dart
new file mode 100644
index 0000000..005b7af
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/vm_trace.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'frame.dart';
+
+/// An implementation of [StackTrace] that emulates the behavior of the VM's
+/// implementation.
+///
+/// In particular, when [toString] is called, this returns a string in the VM's
+/// stack trace format.
+class VMTrace implements StackTrace {
+  /// The stack frames that comprise this stack trace.
+  final List<Frame> frames;
+
+  VMTrace(this.frames);
+
+  @override
+  String toString() {
+    var i = 1;
+    return frames.map((frame) {
+      var number = '#${i++}'.padRight(8);
+      var member = frame.member!
+          .replaceAllMapped(RegExp(r'[^.]+\.<async>'),
+              (match) => '${match[1]}.<${match[1]}_async_body>')
+          .replaceAll('<fn>', '<anonymous closure>');
+      var line = frame.line ?? 0;
+      var column = frame.column ?? 0;
+      return '$number$member (${frame.uri}:$line:$column)\n';
+    }).join();
+  }
+}
diff --git a/pkgs/stack_trace/lib/stack_trace.dart b/pkgs/stack_trace/lib/stack_trace.dart
new file mode 100644
index 0000000..fad30ce
--- /dev/null
+++ b/pkgs/stack_trace/lib/stack_trace.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/chain.dart';
+export 'src/frame.dart';
+export 'src/trace.dart';
+export 'src/unparsed_frame.dart';
diff --git a/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml
new file mode 100644
index 0000000..4f387b1
--- /dev/null
+++ b/pkgs/stack_trace/pubspec.yaml
@@ -0,0 +1,14 @@
+name: stack_trace
+version: 1.12.1
+description: A package for manipulating stack traces and printing them readably.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stack_trace
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  path: ^1.8.0
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test: ^1.16.6
diff --git a/pkgs/stack_trace/test/chain/chain_test.dart b/pkgs/stack_trace/test/chain/chain_test.dart
new file mode 100644
index 0000000..d5426dd
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/chain_test.dart
@@ -0,0 +1,375 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  group('Chain.parse()', () {
+    test('parses a real Chain', () async {
+      // ignore: only_throw_errors
+      final chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+
+      expect(
+        Chain.parse(chain.toString()).toString(),
+        equals(chain.toString()),
+      );
+    });
+
+    test('parses an empty string', () {
+      var chain = Chain.parse('');
+      expect(chain.traces, isEmpty);
+    });
+
+    test('parses a chain containing empty traces', () {
+      var chain =
+          Chain.parse('===== asynchronous gap ===========================\n'
+              '===== asynchronous gap ===========================\n');
+      expect(chain.traces, hasLength(3));
+      expect(chain.traces[0].frames, isEmpty);
+      expect(chain.traces[1].frames, isEmpty);
+      expect(chain.traces[2].frames, isEmpty);
+    });
+
+    test('parses a chain with VM gaps', () {
+      final chain =
+          Chain.parse('#1      MyClass.run (package:my_lib.dart:134:5)\n'
+              '<asynchronous suspension>\n'
+              '#2      main (file:///my_app.dart:9:3)\n'
+              '<asynchronous suspension>\n');
+      expect(chain.traces, hasLength(2));
+      expect(chain.traces[0].frames, hasLength(1));
+      expect(chain.traces[0].frames[0].toString(),
+          equals('package:my_lib.dart 134:5 in MyClass.run'));
+      expect(chain.traces[1].frames, hasLength(1));
+      expect(
+        chain.traces[1].frames[0].toString(),
+        anyOf(
+          equals('/my_app.dart 9:3 in main'), // VM
+          equals('file:///my_app.dart 9:3 in main'), // Browser
+        ),
+      );
+    });
+  });
+
+  group('Chain.capture()', () {
+    test('with onError blocks errors', () {
+      Chain.capture(() {
+        return Future<void>.error('oh no');
+      }, onError: expectAsync2((error, chain) {
+        expect(error, equals('oh no'));
+        expect(chain, isA<Chain>());
+      })).then(expectAsync1((_) {}, count: 0),
+          onError: expectAsync2((_, __) {}, count: 0));
+    });
+
+    test('with no onError blocks errors', () {
+      runZonedGuarded(() {
+        Chain.capture(() => Future<void>.error('oh no')).then(
+            expectAsync1((_) {}, count: 0),
+            onError: expectAsync2((_, __) {}, count: 0));
+      }, expectAsync2((error, chain) {
+        expect(error, equals('oh no'));
+        expect(chain, isA<Chain>());
+      }));
+    });
+
+    test("with errorZone: false doesn't block errors", () {
+      expect(Chain.capture(() => Future<void>.error('oh no'), errorZone: false),
+          throwsA('oh no'));
+    });
+
+    test("doesn't allow onError and errorZone: false", () {
+      expect(() => Chain.capture(() {}, onError: (_, __) {}, errorZone: false),
+          throwsArgumentError);
+    });
+
+    group('with when: false', () {
+      test("with no onError doesn't block errors", () {
+        expect(Chain.capture(() => Future<void>.error('oh no'), when: false),
+            throwsA('oh no'));
+      });
+
+      test('with onError blocks errors', () {
+        Chain.capture(() {
+          return Future<void>.error('oh no');
+        }, onError: expectAsync2((error, chain) {
+          expect(error, equals('oh no'));
+          expect(chain, isA<Chain>());
+        }), when: false);
+      });
+
+      test("doesn't enable chain-tracking", () {
+        return Chain.disable(() {
+          return Chain.capture(() {
+            var completer = Completer<Chain>();
+            inMicrotask(() {
+              completer.complete(Chain.current());
+            });
+
+            return completer.future.then((chain) {
+              expect(chain.traces, hasLength(1));
+            });
+          }, when: false);
+        });
+      });
+    });
+  });
+
+  test('Chain.capture() with custom zoneValues', () {
+    return Chain.capture(() {
+      expect(Zone.current[#enabled], true);
+    }, zoneValues: {#enabled: true});
+  });
+
+  group('Chain.disable()', () {
+    test('disables chain-tracking', () {
+      return Chain.disable(() {
+        var completer = Completer<Chain>();
+        inMicrotask(() => completer.complete(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 = Completer<Chain>();
+          inMicrotask(() => completer.complete(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 = Completer<Chain>();
+          inMicrotask(() => completer.complete(Chain.current()));
+
+          return completer.future.then((chain) {
+            expect(chain.traces, hasLength(2));
+          });
+        }, when: false);
+      });
+    });
+  });
+
+  test('toString() ensures that all traces are aligned', () {
+    var chain = Chain([
+      Trace.parse('short 10:11  Foo.bar\n'),
+      Trace.parse('loooooooooooong 10:11  Zop.zoop')
+    ]);
+
+    expect(
+        chain.toString(),
+        equals('short 10:11            Foo.bar\n'
+            '===== asynchronous gap ===========================\n'
+            'loooooooooooong 10:11  Zop.zoop\n'));
+  });
+
+  var userSlashCode = p.join('user', 'code.dart');
+  group('Chain.terse', () {
+    test('makes each trace terse', () {
+      var chain = Chain([
+        Trace.parse('dart:core 10:11       Foo.bar\n'
+            'dart:core 10:11       Bar.baz\n'
+            'user/code.dart 10:11  Bang.qux\n'
+            'dart:core 10:11       Zip.zap\n'
+            'dart:core 10:11       Zop.zoop'),
+        Trace.parse('user/code.dart 10:11                        Bang.qux\n'
+            'dart:core 10:11                             Foo.bar\n'
+            'package:stack_trace/stack_trace.dart 10:11  Bar.baz\n'
+            'dart:core 10:11                             Zip.zap\n'
+            'user/code.dart 10:11                        Zop.zoop')
+      ]);
+
+      expect(
+          chain.terse.toString(),
+          equals('dart:core             Bar.baz\n'
+              '$userSlashCode 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              '$userSlashCode 10:11  Bang.qux\n'
+              'dart:core             Zip.zap\n'
+              '$userSlashCode 10:11  Zop.zoop\n'));
+    });
+
+    test('eliminates internal-only traces', () {
+      var chain = Chain([
+        Trace.parse('user/code.dart 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz'),
+        Trace.parse('dart:core 10:11                             Foo.bar\n'
+            'package:stack_trace/stack_trace.dart 10:11  Bar.baz\n'
+            'dart:core 10:11                             Zip.zap'),
+        Trace.parse('user/code.dart 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz')
+      ]);
+
+      expect(
+          chain.terse.toString(),
+          equals('$userSlashCode 10:11  Foo.bar\n'
+              '===== asynchronous gap ===========================\n'
+              '$userSlashCode 10:11  Foo.bar\n'));
+    });
+
+    test("doesn't return an empty chain", () {
+      var chain = Chain([
+        Trace.parse('dart:core 10:11                             Foo.bar\n'
+            'package:stack_trace/stack_trace.dart 10:11  Bar.baz\n'
+            'dart:core 10:11                             Zip.zap'),
+        Trace.parse('dart:core 10:11                             A.b\n'
+            'package:stack_trace/stack_trace.dart 10:11  C.d\n'
+            'dart:core 10:11                             E.f')
+      ]);
+
+      expect(chain.terse.toString(), equals('dart:core  E.f\n'));
+    });
+
+    // Regression test for #9
+    test("doesn't crash on empty traces", () {
+      var chain = Chain([
+        Trace.parse('user/code.dart 10:11  Bang.qux'),
+        Trace([]),
+        Trace.parse('user/code.dart 10:11  Bang.qux')
+      ]);
+
+      expect(
+          chain.terse.toString(),
+          equals('$userSlashCode 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              '$userSlashCode 10:11  Bang.qux\n'));
+    });
+  });
+
+  group('Chain.foldFrames', () {
+    test('folds each trace', () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'b.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'a.dart 10:11  Zop.zoop'),
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'a.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(
+          folded.toString(),
+          equals('a.dart 10:11  Bar.baz\n'
+              'b.dart 10:11  Bang.qux\n'
+              'a.dart 10:11  Zop.zoop\n'
+              '===== asynchronous gap ===========================\n'
+              'a.dart 10:11  Zip.zap\n'
+              'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test('with terse: true, folds core frames as well', () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11                        Foo.bar\n'
+            'dart:async-patch/future.dart 10:11  Zip.zap\n'
+            'b.dart 10:11                        Bang.qux\n'
+            'dart:core 10:11                     Bar.baz\n'
+            'a.dart 10:11                        Zop.zoop'),
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bar.baz\n'
+            'a.dart 10:11  Bang.qux\n'
+            'a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded =
+          chain.foldFrames((frame) => frame.library == 'a.dart', terse: true);
+      expect(
+          folded.toString(),
+          equals('dart:async    Zip.zap\n'
+              'b.dart 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              'a.dart        Zip.zap\n'
+              'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test('eliminates completely-folded traces', () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'b.dart 10:11  Bang.qux'),
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bang.qux'),
+        Trace.parse('a.dart 10:11  Zip.zap\n'
+            'b.dart 10:11  Zop.zoop')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(
+          folded.toString(),
+          equals('a.dart 10:11  Foo.bar\n'
+              'b.dart 10:11  Bang.qux\n'
+              '===== asynchronous gap ===========================\n'
+              'a.dart 10:11  Zip.zap\n'
+              'b.dart 10:11  Zop.zoop\n'));
+    });
+
+    test("doesn't return an empty trace", () {
+      var chain = Chain([
+        Trace.parse('a.dart 10:11  Foo.bar\n'
+            'a.dart 10:11  Bang.qux')
+      ]);
+
+      var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+      expect(folded.toString(), equals('a.dart 10:11  Bang.qux\n'));
+    });
+  });
+
+  test('Chain.toTrace eliminates asynchronous gaps', () {
+    var trace = Chain([
+      Trace.parse('user/code.dart 10:11  Foo.bar\n'
+          'dart:core 10:11       Bar.baz'),
+      Trace.parse('user/code.dart 10:11  Foo.bar\n'
+          'dart:core 10:11       Bar.baz')
+    ]).toTrace();
+
+    expect(
+        trace.toString(),
+        equals('$userSlashCode 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz\n'
+            '$userSlashCode 10:11  Foo.bar\n'
+            'dart:core 10:11       Bar.baz\n'));
+  });
+}
diff --git a/pkgs/stack_trace/test/chain/dart2js_test.dart b/pkgs/stack_trace/test/chain/dart2js_test.dart
new file mode 100644
index 0000000..abb842d
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/dart2js_test.dart
@@ -0,0 +1,337 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: only_throw_errors
+
+// dart2js chain tests are separated out because dart2js stack traces are
+// inconsistent due to inlining and browser differences. These tests don't
+// assert anything about the content of the traces, just the number of traces in
+// a chain.
+@TestOn('js')
+library;
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  group('capture() with onError catches exceptions', () {
+    test('thrown synchronously', () async {
+      var chain = await captureFuture(() => throw 'error');
+      expect(chain.traces, hasLength(1));
+    });
+
+    test('thrown in a microtask', () async {
+      var chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in a one-shot timer', () async {
+      var chain =
+          await captureFuture(() => inOneShotTimer(() => throw 'error'));
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in a periodic timer', () async {
+      var chain =
+          await captureFuture(() => inPeriodicTimer(() => throw 'error'));
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in a nested series of asynchronous operations', () async {
+      var chain = await captureFuture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() => inMicrotask(() => throw 'error'));
+        });
+      });
+
+      expect(chain.traces, hasLength(4));
+    });
+
+    test('thrown in a long future chain', () async {
+      var chain = await captureFuture(() => inFutureChain(() => throw 'error'));
+
+      // Despite many asynchronous operations, there's only one level of
+      // nested calls, so there should be only two traces in the chain. This
+      // is important; programmers expect stack trace memory consumption to be
+      // O(depth of program), not O(length of program).
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('thrown in new Future()', () async {
+      var chain = await captureFuture(() => inNewFuture(() => throw 'error'));
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('thrown in new Future.sync()', () async {
+      var chain = await captureFuture(() {
+        inMicrotask(() => inSyncFuture(() => throw 'error'));
+      });
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('multiple times', () {
+      var completer = Completer<void>();
+      var first = true;
+
+      Chain.capture(() {
+        inMicrotask(() => throw 'first error');
+        inPeriodicTimer(() => throw 'second error');
+      }, onError: (error, chain) {
+        try {
+          if (first) {
+            expect(error, equals('first error'));
+            expect(chain.traces, hasLength(2));
+            first = false;
+          } else {
+            expect(error, equals('second error'));
+            expect(chain.traces, hasLength(2));
+            completer.complete();
+          }
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+
+    test('passed to a completer', () async {
+      var trace = Trace.current();
+      var chain = await captureFuture(() {
+        inMicrotask(() => completerErrorFuture(trace));
+      });
+
+      expect(chain.traces, hasLength(3));
+
+      // The first trace is the trace that was manually reported for the
+      // error.
+      expect(chain.traces.first.toString(), equals(trace.toString()));
+    });
+
+    test('passed to a completer with no stack trace', () async {
+      var chain = await captureFuture(() {
+        inMicrotask(completerErrorFuture);
+      });
+
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('passed to a stream controller', () async {
+      var trace = Trace.current();
+      var chain = await captureFuture(() {
+        inMicrotask(() => controllerErrorStream(trace).listen(null));
+      });
+
+      expect(chain.traces, hasLength(3));
+      expect(chain.traces.first.toString(), equals(trace.toString()));
+    });
+
+    test('passed to a stream controller with no stack trace', () async {
+      var chain = await captureFuture(() {
+        inMicrotask(() => controllerErrorStream().listen(null));
+      });
+
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('and relays them to the parent zone', () {
+      var completer = Completer<void>();
+
+      runZonedGuarded(() {
+        Chain.capture(() {
+          inMicrotask(() => throw 'error');
+        }, onError: (error, chain) {
+          expect(error, equals('error'));
+          expect(chain.traces, hasLength(2));
+          throw error;
+        });
+      }, (error, chain) {
+        try {
+          expect(error, equals('error'));
+          expect(chain,
+              isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+          completer.complete();
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+  });
+
+  test('capture() without onError passes exceptions to parent zone', () {
+    var completer = Completer<void>();
+
+    runZonedGuarded(() {
+      Chain.capture(() => inMicrotask(() => throw 'error'));
+    }, (error, chain) {
+      try {
+        expect(error, equals('error'));
+        expect(chain,
+            isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+        completer.complete();
+      } on Object catch (error, stackTrace) {
+        completer.completeError(error, stackTrace);
+      }
+    });
+
+    return completer.future;
+  });
+
+  group('current() within capture()', () {
+    test('called in a microtask', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inMicrotask(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('called in a one-shot timer', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inOneShotTimer(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('called in a periodic timer', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+
+    test('called in a nested series of asynchronous operations', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() {
+            inMicrotask(() => completer.complete(Chain.current()));
+          });
+        });
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(4));
+    });
+
+    test('called in a long future chain', () async {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inFutureChain(() => completer.complete(Chain.current()));
+      });
+
+      var chain = await completer.future;
+      expect(chain.traces, hasLength(2));
+    });
+  });
+
+  test(
+    'current() outside of capture() returns a chain wrapping the current trace',
+    () =>
+        // The test runner runs all tests with chains enabled.
+        Chain.disable(() async {
+      var completer = Completer<Chain>();
+      inMicrotask(() => completer.complete(Chain.current()));
+
+      var chain = await completer.future;
+      // Since the chain wasn't loaded within [Chain.capture], the full stack
+      // chain isn't available and it just returns the current stack when
+      // called.
+      expect(chain.traces, hasLength(1));
+    }),
+  );
+
+  group('forTrace() within capture()', () {
+    test('called for a stack trace from a microtask', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inMicrotask, () => throw 'error'));
+
+      // Because [chainForTrace] has to set up a future chain to capture the
+      // stack trace while still showing it to the zone specification, it adds
+      // an additional level of async nesting and so an additional trace.
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('called for a stack trace from a one-shot timer', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inOneShotTimer, () => throw 'error'));
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test('called for a stack trace from a periodic timer', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inPeriodicTimer, () => throw 'error'));
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test(
+        'called for a stack trace from a nested series of asynchronous '
+        'operations', () async {
+      var chain = await Chain.capture(() => chainForTrace((callback) {
+            inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+          }, () => throw 'error'));
+
+      expect(chain.traces, hasLength(5));
+    });
+
+    test('called for a stack trace from a long future chain', () async {
+      var chain = await Chain.capture(
+          () => chainForTrace(inFutureChain, () => throw 'error'));
+
+      expect(chain.traces, hasLength(3));
+    });
+
+    test(
+        'called for an unregistered stack trace returns a chain wrapping that '
+        'trace', () {
+      late StackTrace trace;
+      var chain = Chain.capture(() {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          trace = stackTrace;
+          return Chain.forTrace(stackTrace);
+        }
+      });
+
+      expect(chain.traces, hasLength(1));
+      expect(
+          chain.traces.first.toString(), equals(Trace.from(trace).toString()));
+    });
+  });
+
+  test(
+      'forTrace() outside of capture() returns a chain wrapping the given '
+      'trace', () {
+    late StackTrace trace;
+    var chain = Chain.capture(() {
+      try {
+        throw 'error';
+      } catch (_, stackTrace) {
+        trace = stackTrace;
+        return Chain.forTrace(stackTrace);
+      }
+    });
+
+    expect(chain.traces, hasLength(1));
+    expect(chain.traces.first.toString(), equals(Trace.from(trace).toString()));
+  });
+}
diff --git a/pkgs/stack_trace/test/chain/utils.dart b/pkgs/stack_trace/test/chain/utils.dart
new file mode 100644
index 0000000..27fb0e6
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/utils.dart
@@ -0,0 +1,94 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// Runs [callback] in a microtask callback.
+void inMicrotask(void Function() callback) => scheduleMicrotask(callback);
+
+/// Runs [callback] in a one-shot timer callback.
+void inOneShotTimer(void Function() callback) => Timer.run(callback);
+
+/// Runs [callback] once in a periodic timer callback.
+void inPeriodicTimer(void Function() callback) {
+  var count = 0;
+  Timer.periodic(const Duration(milliseconds: 1), (timer) {
+    count++;
+    if (count != 5) return;
+    timer.cancel();
+    callback();
+  });
+}
+
+/// Runs [callback] within a long asynchronous Future chain.
+void inFutureChain(void Function() callback) {
+  Future(() {})
+      .then((_) => Future(() {}))
+      .then((_) => Future(() {}))
+      .then((_) => Future(() {}))
+      .then((_) => Future(() {}))
+      .then((_) => callback())
+      .then((_) => Future(() {}));
+}
+
+void inNewFuture(void Function() callback) {
+  Future(callback);
+}
+
+void inSyncFuture(void Function() callback) {
+  Future.sync(callback);
+}
+
+/// Returns a Future that completes to an error using a completer.
+///
+/// If [trace] is passed, it's used as the stack trace for the error.
+Future<void> completerErrorFuture([StackTrace? trace]) {
+  var completer = Completer<void>();
+  completer.completeError('error', trace);
+  return completer.future;
+}
+
+/// Returns a Stream that emits an error using a controller.
+///
+/// If [trace] is passed, it's used as the stack trace for the error.
+Stream<void> controllerErrorStream([StackTrace? trace]) {
+  var controller = StreamController<void>();
+  controller.addError('error', trace);
+  return controller.stream;
+}
+
+/// Runs [callback] within [asyncFn], then converts any errors raised into a
+/// [Chain] with [Chain.forTrace].
+Future<Chain> chainForTrace(
+    void Function(void Function()) asyncFn, void Function() callback) {
+  var completer = Completer<Chain>();
+  asyncFn(() {
+    // We use `new Future.value().then(...)` here as opposed to [new Future] or
+    // [new Future.sync] because those methods don't pass the exception through
+    // the zone specification before propagating it, so there's no chance to
+    // attach a chain to its stack trace. See issue 15105.
+    Future<void>.value()
+        .then((_) => callback())
+        .catchError(completer.completeError);
+  });
+
+  return completer.future
+      .catchError((_, StackTrace stackTrace) => Chain.forTrace(stackTrace));
+}
+
+/// Runs [callback] in a [Chain.capture] zone and returns a Future that
+/// completes to the stack chain for an error thrown by [callback].
+///
+/// [callback] is expected to throw the string `"error"`.
+Future<Chain> captureFuture(void Function() callback) {
+  var completer = Completer<Chain>();
+  Chain.capture(callback, onError: (error, chain) {
+    expect(error, equals('error'));
+    completer.complete(chain);
+  });
+  return completer.future;
+}
diff --git a/pkgs/stack_trace/test/chain/vm_test.dart b/pkgs/stack_trace/test/chain/vm_test.dart
new file mode 100644
index 0000000..5c6c0b7
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/vm_test.dart
@@ -0,0 +1,508 @@
+// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: only_throw_errors
+
+// VM chain tests can rely on stronger guarantees about the contents of the
+// stack traces than dart2js.
+@TestOn('dart-vm')
+library;
+
+import 'dart:async';
+
+import 'package:stack_trace/src/utils.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+import 'utils.dart';
+
+void main() {
+  group('capture() with onError catches exceptions', () {
+    test('thrown synchronously', () async {
+      late StackTrace vmTrace;
+      var chain = await captureFuture(() {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          vmTrace = stackTrace;
+          rethrow;
+        }
+      });
+
+      // Because there's no chain context for a synchronous error, we fall back
+      // on the VM's stack chain tracking.
+      expect(
+          chain.toString(), equals(Chain.parse(vmTrace.toString()).toString()));
+    });
+
+    test('thrown in a microtask', () {
+      return captureFuture(() => inMicrotask(() => throw 'error'))
+          .then((chain) {
+        // Since there was only one asynchronous operation, there should be only
+        // two traces in the chain.
+        expect(chain.traces, hasLength(2));
+
+        // The first frame of the first trace should be the line on which the
+        // actual error was thrown.
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+
+        // The second trace should describe the stack when the error callback
+        // was scheduled.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('thrown in a one-shot timer', () {
+      return captureFuture(() => inOneShotTimer(() => throw 'error'))
+          .then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+      });
+    });
+
+    test('thrown in a periodic timer', () {
+      return captureFuture(() => inPeriodicTimer(() => throw 'error'))
+          .then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('thrown in a nested series of asynchronous operations', () {
+      return captureFuture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() => inMicrotask(() => throw 'error'));
+        });
+      }).then((chain) {
+        expect(chain.traces, hasLength(4));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+        expect(chain.traces[3].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('thrown in a long future chain', () {
+      return captureFuture(() => inFutureChain(() => throw 'error'))
+          .then((chain) {
+        // Despite many asynchronous operations, there's only one level of
+        // nested calls, so there should be only two traces in the chain. This
+        // is important; programmers expect stack trace memory consumption to be
+        // O(depth of program), not O(length of program).
+        expect(chain.traces, hasLength(2));
+
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inFutureChain'))));
+      });
+    });
+
+    test('thrown in new Future()', () {
+      return captureFuture(() => inNewFuture(() => throw 'error'))
+          .then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+
+        // The second trace is the one captured by
+        // [StackZoneSpecification.errorCallback]. Because that runs
+        // asynchronously within [new Future], it doesn't actually refer to the
+        // source file at all.
+        expect(chain.traces[1].frames,
+            everyElement(frameLibrary(isNot(contains('chain_test')))));
+
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inNewFuture'))));
+      });
+    });
+
+    test('thrown in new Future.sync()', () {
+      return captureFuture(() {
+        inMicrotask(() => inSyncFuture(() => throw 'error'));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inSyncFuture'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('multiple times', () {
+      var completer = Completer<void>();
+      var first = true;
+
+      Chain.capture(() {
+        inMicrotask(() => throw 'first error');
+        inPeriodicTimer(() => throw 'second error');
+      }, onError: (error, chain) {
+        try {
+          if (first) {
+            expect(error, equals('first error'));
+            expect(chain.traces[1].frames,
+                contains(frameMember(startsWith('inMicrotask'))));
+            first = false;
+          } else {
+            expect(error, equals('second error'));
+            expect(chain.traces[1].frames,
+                contains(frameMember(startsWith('inPeriodicTimer'))));
+            completer.complete();
+          }
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+
+    test('passed to a completer', () {
+      var trace = Trace.current();
+      return captureFuture(() {
+        inMicrotask(() => completerErrorFuture(trace));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+
+        // The first trace is the trace that was manually reported for the
+        // error.
+        expect(chain.traces.first.toString(), equals(trace.toString()));
+
+        // The second trace is the trace that was captured when
+        // [Completer.addError] was called.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('completerErrorFuture'))));
+
+        // The third trace is the automatically-captured trace from when the
+        // microtask was scheduled.
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a completer with no stack trace', () {
+      return captureFuture(() {
+        inMicrotask(completerErrorFuture);
+      }).then((chain) {
+        expect(chain.traces, hasLength(2));
+
+        // The first trace is the one captured when [Completer.addError] was
+        // called.
+        expect(chain.traces[0].frames,
+            contains(frameMember(startsWith('completerErrorFuture'))));
+
+        // The second trace is the automatically-captured trace from when the
+        // microtask was scheduled.
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a stream controller', () {
+      var trace = Trace.current();
+      return captureFuture(() {
+        inMicrotask(() => controllerErrorStream(trace).listen(null));
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces.first.toString(), equals(trace.toString()));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('controllerErrorStream'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('passed to a stream controller with no stack trace', () {
+      return captureFuture(() {
+        inMicrotask(() => controllerErrorStream().listen(null));
+      }).then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames,
+            contains(frameMember(startsWith('controllerErrorStream'))));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('and relays them to the parent zone', () {
+      var completer = Completer<void>();
+
+      runZonedGuarded(() {
+        Chain.capture(() {
+          inMicrotask(() => throw 'error');
+        }, onError: (error, chain) {
+          expect(error, equals('error'));
+          expect(chain.traces[1].frames,
+              contains(frameMember(startsWith('inMicrotask'))));
+          throw error;
+        });
+      }, (error, chain) {
+        try {
+          expect(error, equals('error'));
+          expect(
+              chain,
+              isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+                  contains(frameMember(startsWith('inMicrotask')))));
+          completer.complete();
+        } on Object catch (error, stackTrace) {
+          completer.completeError(error, stackTrace);
+        }
+      });
+
+      return completer.future;
+    });
+  });
+
+  test('capture() without onError passes exceptions to parent zone', () {
+    var completer = Completer<void>();
+
+    runZonedGuarded(() {
+      Chain.capture(() => inMicrotask(() => throw 'error'));
+    }, (error, chain) {
+      try {
+        expect(error, equals('error'));
+        expect(
+            chain,
+            isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+                contains(frameMember(startsWith('inMicrotask')))));
+        completer.complete();
+      } on Object catch (error, stackTrace) {
+        completer.completeError(error, stackTrace);
+      }
+    });
+
+    return completer.future;
+  });
+
+  group('current() within capture()', () {
+    test('called in a microtask', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inMicrotask(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('called in a one-shot timer', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inOneShotTimer(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+      });
+    });
+
+    test('called in a periodic timer', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('called in a nested series of asynchronous operations', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inPeriodicTimer(() {
+          inOneShotTimer(() {
+            inMicrotask(() => completer.complete(Chain.current()));
+          });
+        });
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(4));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+        expect(chain.traces[3].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('called in a long future chain', () {
+      var completer = Completer<Chain>();
+      Chain.capture(() {
+        inFutureChain(() => completer.complete(Chain.current()));
+      });
+
+      return completer.future.then((chain) {
+        expect(chain.traces, hasLength(2));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('inFutureChain'))));
+      });
+    });
+  });
+
+  test(
+      'current() outside of capture() returns a chain wrapping the current '
+      'trace', () {
+    // The test runner runs all tests with chains enabled.
+    return Chain.disable(() {
+      var completer = Completer<Chain>();
+      inMicrotask(() => completer.complete(Chain.current()));
+
+      return completer.future.then((chain) {
+        // Since the chain wasn't loaded within [Chain.capture], the full stack
+        // chain isn't available and it just returns the current stack when
+        // called.
+        expect(chain.traces, hasLength(1));
+        expect(
+            chain.traces.first.frames.first, frameMember(startsWith('main')));
+      });
+    });
+  });
+
+  group('forTrace() within capture()', () {
+    test('called for a stack trace from a microtask', () {
+      return Chain.capture(() {
+        return chainForTrace(inMicrotask, () => throw 'error');
+      }).then((chain) {
+        // Because [chainForTrace] has to set up a future chain to capture the
+        // stack trace while still showing it to the zone specification, it adds
+        // an additional level of async nesting and so an additional trace.
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+      });
+    });
+
+    test('called for a stack trace from a one-shot timer', () {
+      return Chain.capture(() {
+        return chainForTrace(inOneShotTimer, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+      });
+    });
+
+    test('called for a stack trace from a periodic timer', () {
+      return Chain.capture(() {
+        return chainForTrace(inPeriodicTimer, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test(
+        'called for a stack trace from a nested series of asynchronous '
+        'operations', () {
+      return Chain.capture(() {
+        return chainForTrace((callback) {
+          inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+        }, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(5));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inMicrotask'))));
+        expect(chain.traces[3].frames,
+            contains(frameMember(startsWith('inOneShotTimer'))));
+        expect(chain.traces[4].frames,
+            contains(frameMember(startsWith('inPeriodicTimer'))));
+      });
+    });
+
+    test('called for a stack trace from a long future chain', () {
+      return Chain.capture(() {
+        return chainForTrace(inFutureChain, () => throw 'error');
+      }).then((chain) {
+        expect(chain.traces, hasLength(3));
+        expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+        expect(chain.traces[1].frames,
+            contains(frameMember(startsWith('chainForTrace'))));
+        expect(chain.traces[2].frames,
+            contains(frameMember(startsWith('inFutureChain'))));
+      });
+    });
+
+    test('called for an unregistered stack trace uses the current chain',
+        () async {
+      late StackTrace trace;
+      var chain = await Chain.capture(() async {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          trace = stackTrace;
+          return Chain.forTrace(stackTrace);
+        }
+      });
+
+      expect(chain.traces, hasLength(greaterThan(1)));
+
+      // Assert that we've trimmed the VM's stack chains here to avoid
+      // duplication.
+      expect(chain.traces.first.toString(),
+          equals(Chain.parse(trace.toString()).traces.first.toString()));
+    });
+  });
+
+  test(
+      'forTrace() outside of capture() returns a chain describing the VM stack '
+      'chain', () {
+    // Disable the test package's chain-tracking.
+    return Chain.disable(() async {
+      late StackTrace trace;
+      await Chain.capture(() async {
+        try {
+          throw 'error';
+        } catch (_, stackTrace) {
+          trace = stackTrace;
+        }
+      });
+
+      final chain = Chain.forTrace(trace);
+      final traceStr = trace.toString();
+      final gaps = vmChainGap.allMatches(traceStr);
+      // If the trace ends on a gap, there's no sub-trace following the gap.
+      final expectedLength =
+          (gaps.last.end == traceStr.length) ? gaps.length : gaps.length + 1;
+      expect(chain.traces, hasLength(expectedLength));
+      expect(
+          chain.traces.first.frames, contains(frameMember(startsWith('main'))));
+    });
+  });
+}
diff --git a/pkgs/stack_trace/test/frame_test.dart b/pkgs/stack_trace/test/frame_test.dart
new file mode 100644
index 0000000..a5dfc20
--- /dev/null
+++ b/pkgs/stack_trace/test/frame_test.dart
@@ -0,0 +1,729 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('.parseVM', () {
+    test('parses a stack frame with column correctly', () {
+      var frame = Frame.parseVM('#1      Foo._bar '
+          '(file:///home/nweiz/code/stuff.dart:42:21)');
+      expect(
+          frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(frame.line, equals(42));
+      expect(frame.column, equals(21));
+      expect(frame.member, equals('Foo._bar'));
+    });
+
+    test('parses a stack frame without column correctly', () {
+      var frame = Frame.parseVM('#1      Foo._bar '
+          '(file:///home/nweiz/code/stuff.dart:24)');
+      expect(
+          frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(frame.line, equals(24));
+      expect(frame.column, null);
+      expect(frame.member, equals('Foo._bar'));
+    });
+
+    // This can happen with async stack traces. See issue 22009.
+    test('parses a stack frame without line or column correctly', () {
+      var frame = Frame.parseVM('#1      Foo._bar '
+          '(file:///home/nweiz/code/stuff.dart)');
+      expect(
+          frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Foo._bar'));
+    });
+
+    test('converts "<anonymous closure>" to "<fn>"', () {
+      String? parsedMember(String member) =>
+          Frame.parseVM('#0 $member (foo:0:0)').member;
+
+      expect(parsedMember('Foo.<anonymous closure>'), equals('Foo.<fn>'));
+      expect(parsedMember('<anonymous closure>.<anonymous closure>.bar'),
+          equals('<fn>.<fn>.bar'));
+    });
+
+    test('converts "<<anonymous closure>_async_body>" to "<async>"', () {
+      var frame =
+          Frame.parseVM('#0 Foo.<<anonymous closure>_async_body> (foo:0:0)');
+      expect(frame.member, equals('Foo.<async>'));
+    });
+
+    test('converts "<function_name_async_body>" to "<async>"', () {
+      var frame = Frame.parseVM('#0 Foo.<function_name_async_body> (foo:0:0)');
+      expect(frame.member, equals('Foo.<async>'));
+    });
+
+    test('parses a folded frame correctly', () {
+      var frame = Frame.parseVM('...');
+
+      expect(frame.member, equals('...'));
+      expect(frame.uri, equals(Uri()));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+    });
+  });
+
+  group('.parseV8', () {
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseV8, '');
+      expectIsUnparsed(Frame.parseV8, '#1');
+      expectIsUnparsed(Frame.parseV8, '#1      Foo');
+      expectIsUnparsed(Frame.parseV8, '#1      (dart:async/future.dart:10:15)');
+      expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+    });
+
+    test('parses a stack frame correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(https://example.com/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a : in the authority', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(http://localhost:8080/stuff.dart.js:560:28)');
+      expect(
+          frame.uri, equals(Uri.parse('http://localhost:8080/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute POSIX path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(/path/to/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute Windows path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          r'(C:\path\to\stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a Windows UNC path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          r'(\\mount\path\to\stuff.dart.js:560:28)');
+      expect(
+          frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative POSIX path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          '(path/to/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative Windows path correctly', () {
+      var frame = Frame.parseV8('    at VW.call\$0 '
+          r'(path\to\stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses an anonymous stack frame correctly', () {
+      var frame =
+          Frame.parseV8('    at https://example.com/stuff.dart.js:560:28');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('<fn>'));
+    });
+
+    test('parses a native stack frame correctly', () {
+      var frame = Frame.parseV8('    at Object.stringify (native)');
+      expect(frame.uri, Uri.parse('native'));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Object.stringify'));
+    });
+
+    test('parses a stack frame with [as ...] correctly', () {
+      // Ignore "[as ...]", since other stack trace formats don't support a
+      // similar construct.
+      var frame = Frame.parseV8('    at VW.call\$0 [as call\$4] '
+          '(https://example.com/stuff.dart.js:560:28)');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a basic eval stack frame correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at <anonymous> '
+          '(https://example.com/stuff.dart.js:560:28))');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('parses an IE10 eval stack frame correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at Anonymous function '
+          '(https://example.com/stuff.dart.js:560:28))');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('parses an eval stack frame with inner position info correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at <anonymous> '
+          '(https://example.com/stuff.dart.js:560:28), <anonymous>:3:28)');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('parses a nested eval stack frame correctly', () {
+      var frame = Frame.parseV8('    at eval (eval at <anonymous> '
+          '(eval at sub (https://example.com/stuff.dart.js:560:28)))');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, equals(28));
+      expect(frame.member, equals('eval'));
+    });
+
+    test('converts "<anonymous>" to "<fn>"', () {
+      String? parsedMember(String member) =>
+          Frame.parseV8('    at $member (foo:0:0)').member;
+
+      expect(parsedMember('Foo.<anonymous>'), equals('Foo.<fn>'));
+      expect(
+          parsedMember('<anonymous>.<anonymous>.bar'), equals('<fn>.<fn>.bar'));
+    });
+
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseV8, '');
+      expectIsUnparsed(Frame.parseV8, '    at');
+      expectIsUnparsed(Frame.parseV8, '    at Foo');
+      expectIsUnparsed(Frame.parseV8, '    at Foo (dart:async/future.dart)');
+      expectIsUnparsed(Frame.parseV8, '    at (dart:async/future.dart:10:15)');
+      expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+      expectIsUnparsed(Frame.parseV8, '    at dart:async/future.dart');
+      expectIsUnparsed(Frame.parseV8, 'dart:async/future.dart:10:15');
+    });
+  });
+
+  group('.parseFirefox/.parseSafari', () {
+    test('parses a Firefox stack trace with anonymous function', () {
+      var trace = Trace.parse('''
+Foo._bar@https://example.com/stuff.js:18056:12
+anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+baz@https://pub.dev/buz.js:56355:55
+        ''');
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].line, equals(18056));
+      expect(trace.frames[0].column, equals(12));
+      expect(trace.frames[0].member, equals('Foo._bar'));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].line, equals(693));
+      expect(trace.frames[1].column, isNull);
+      expect(trace.frames[1].member, equals('<fn>'));
+      expect(trace.frames[2].uri, equals(Uri.parse('https://pub.dev/buz.js')));
+      expect(trace.frames[2].line, equals(56355));
+      expect(trace.frames[2].column, equals(55));
+      expect(trace.frames[2].member, equals('baz'));
+    });
+
+    test('parses a Firefox stack trace with nested evals in anonymous function',
+        () {
+      var trace = Trace.parse('''
+        Foo._bar@https://example.com/stuff.js:18056:12
+        anonymous@file:///C:/example.html line 7 > eval line 1 > eval:1:1
+        anonymous@file:///C:/example.html line 45 > Function:1:1
+        ''');
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].line, equals(18056));
+      expect(trace.frames[0].column, equals(12));
+      expect(trace.frames[0].member, equals('Foo._bar'));
+      expect(trace.frames[1].uri, equals(Uri.parse('file:///C:/example.html')));
+      expect(trace.frames[1].line, equals(7));
+      expect(trace.frames[1].column, isNull);
+      expect(trace.frames[1].member, equals('<fn>'));
+      expect(trace.frames[2].uri, equals(Uri.parse('file:///C:/example.html')));
+      expect(trace.frames[2].line, equals(45));
+      expect(trace.frames[2].column, isNull);
+      expect(trace.frames[2].member, equals('<fn>'));
+    });
+
+    test('parses a simple stack frame correctly', () {
+      var frame = Frame.parseFirefox(
+          '.VW.call\$0@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute POSIX path correctly', () {
+      var frame = Frame.parseFirefox('.VW.call\$0@/path/to/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with an absolute Windows path correctly', () {
+      var frame =
+          Frame.parseFirefox(r'.VW.call$0@C:\path\to\stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a Windows UNC path correctly', () {
+      var frame =
+          Frame.parseFirefox(r'.VW.call$0@\\mount\path\to\stuff.dart.js:560');
+      expect(
+          frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative POSIX path correctly', () {
+      var frame = Frame.parseFirefox('.VW.call\$0@path/to/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a stack frame with a relative Windows path correctly', () {
+      var frame = Frame.parseFirefox(r'.VW.call$0@path\to\stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('VW.call\$0'));
+    });
+
+    test('parses a simple anonymous stack frame correctly', () {
+      var frame = Frame.parseFirefox('@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('<fn>'));
+    });
+
+    test('parses a nested anonymous stack frame correctly', () {
+      var frame =
+          Frame.parseFirefox('.foo/<@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+
+      frame = Frame.parseFirefox('.foo/@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+    });
+
+    test('parses a named nested anonymous stack frame correctly', () {
+      var frame = Frame.parseFirefox(
+          '.foo/.name<@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+
+      frame = Frame.parseFirefox(
+          '.foo/.name@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+    });
+
+    test('parses a stack frame with parameters correctly', () {
+      var frame = Frame.parseFirefox(
+          '.foo(12, "@)()/<")@https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo'));
+    });
+
+    test('parses a nested anonymous stack frame with parameters correctly', () {
+      var frame = Frame.parseFirefox(
+        '.foo(12, "@)()/<")/.fn<@https://example.com/stuff.dart.js:560',
+      );
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo.<fn>'));
+    });
+
+    test(
+        'parses a deeply-nested anonymous stack frame with parameters '
+        'correctly', () {
+      var frame = Frame.parseFirefox('.convertDartClosureToJS/\$function</<@'
+          'https://example.com/stuff.dart.js:560');
+      expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+      expect(frame.line, equals(560));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('convertDartClosureToJS.<fn>.<fn>'));
+    });
+
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseFirefox, '');
+      expectIsUnparsed(Frame.parseFirefox, '.foo');
+      expectIsUnparsed(Frame.parseFirefox, '.foo@dart:async/future.dart');
+      expectIsUnparsed(Frame.parseFirefox, '.foo(@dart:async/future.dart:10');
+      expectIsUnparsed(Frame.parseFirefox, '@dart:async/future.dart');
+    });
+
+    test('parses a simple stack frame correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses an anonymous stack frame correctly', () {
+      var frame = Frame.parseFirefox('https://dart.dev/foo/bar.dart:10:11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('<fn>'));
+    });
+
+    test('parses a stack frame with no line correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart::11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses a stack frame with no column correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('foo\$bar'));
+    });
+
+    test('parses a stack frame with no line or column correctly', () {
+      var frame =
+          Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('foo\$bar'));
+    });
+  });
+
+  group('.parseFriendly', () {
+    test('parses a simple stack frame correctly', () {
+      var frame = Frame.parseFriendly(
+          'https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('parses a stack frame with no line or column correctly', () {
+      var frame =
+          Frame.parseFriendly('https://dart.dev/foo/bar.dart  Foo.<fn>.bar');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('parses a stack frame with no column correctly', () {
+      var frame =
+          Frame.parseFriendly('https://dart.dev/foo/bar.dart 10  Foo.<fn>.bar');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, equals(10));
+      expect(frame.column, isNull);
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('parses a stack frame with a relative path correctly', () {
+      var frame = Frame.parseFriendly('foo/bar.dart 10:11    Foo.<fn>.bar');
+      expect(frame.uri,
+          equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('Foo.<fn>.bar'));
+    });
+
+    test('returns an UnparsedFrame for malformed frames', () {
+      expectIsUnparsed(Frame.parseFriendly, '');
+      expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart');
+      expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart 10:11');
+    });
+
+    test('parses a data url stack frame with no line or column correctly', () {
+      var frame = Frame.parseFriendly('data:...  main');
+      expect(frame.uri.scheme, equals('data'));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('main'));
+    });
+
+    test('parses a data url stack frame correctly', () {
+      var frame = Frame.parseFriendly('data:... 10:11    main');
+      expect(frame.uri.scheme, equals('data'));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('main'));
+    });
+
+    test('parses a stack frame with spaces in the member name correctly', () {
+      var frame = Frame.parseFriendly(
+          'foo/bar.dart 10:11    (anonymous function).dart.fn');
+      expect(frame.uri,
+          equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+      expect(frame.line, equals(10));
+      expect(frame.column, equals(11));
+      expect(frame.member, equals('(anonymous function).dart.fn'));
+    });
+
+    test(
+        'parses a stack frame with spaces in the member name and no line or '
+        'column correctly', () {
+      var frame = Frame.parseFriendly(
+          'https://dart.dev/foo/bar.dart  (anonymous function).dart.fn');
+      expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(frame.line, isNull);
+      expect(frame.column, isNull);
+      expect(frame.member, equals('(anonymous function).dart.fn'));
+    });
+  });
+
+  test('only considers dart URIs to be core', () {
+    bool isCore(String library) =>
+        Frame.parseVM('#0 Foo ($library:0:0)').isCore;
+
+    expect(isCore('dart:core'), isTrue);
+    expect(isCore('dart:async'), isTrue);
+    expect(isCore('dart:core/uri.dart'), isTrue);
+    expect(isCore('dart:async/future.dart'), isTrue);
+    expect(isCore('bart:core'), isFalse);
+    expect(isCore('sdart:core'), isFalse);
+    expect(isCore('darty:core'), isFalse);
+    expect(isCore('bart:core/uri.dart'), isFalse);
+  });
+
+  group('.library', () {
+    test('returns the URI string for non-file URIs', () {
+      expect(Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').library,
+          equals('dart:async/future.dart'));
+      expect(
+          Frame.parseVM('#0 Foo '
+                  '(https://dart.dev/stuff/thing.dart:0:0)')
+              .library,
+          equals('https://dart.dev/stuff/thing.dart'));
+    });
+
+    test('returns the relative path for file URIs', () {
+      expect(Frame.parseVM('#0 Foo (foo/bar.dart:0:0)').library,
+          equals(path.join('foo', 'bar.dart')));
+    });
+
+    test('truncates legacy data: URIs', () {
+      var frame = Frame.parseVM(
+          '#0 Foo (data:application/dart;charset=utf-8,blah:0:0)');
+      expect(frame.library, equals('data:...'));
+    });
+
+    test('truncates data: URIs', () {
+      var frame = Frame.parseVM(
+          '#0      main (<data:application/dart;charset=utf-8>:1:15)');
+      expect(frame.library, equals('data:...'));
+    });
+  });
+
+  group('.location', () {
+    test(
+        'returns the library and line/column numbers for non-core '
+        'libraries', () {
+      expect(
+          Frame.parseVM('#0 Foo '
+                  '(https://dart.dev/thing.dart:5:10)')
+              .location,
+          equals('https://dart.dev/thing.dart 5:10'));
+      expect(Frame.parseVM('#0 Foo (foo/bar.dart:1:2)').location,
+          equals('${path.join('foo', 'bar.dart')} 1:2'));
+    });
+  });
+
+  group('.package', () {
+    test('returns null for non-package URIs', () {
+      expect(
+          Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').package, isNull);
+      expect(
+          Frame.parseVM('#0 Foo '
+                  '(https://dart.dev/stuff/thing.dart:0:0)')
+              .package,
+          isNull);
+    });
+
+    test('returns the package name for package: URIs', () {
+      expect(Frame.parseVM('#0 Foo (package:foo/foo.dart:0:0)').package,
+          equals('foo'));
+      expect(Frame.parseVM('#0 Foo (package:foo/zap/bar.dart:0:0)').package,
+          equals('foo'));
+    });
+  });
+
+  group('.toString()', () {
+    test(
+        'returns the library and line/column numbers for non-core '
+        'libraries', () {
+      expect(
+          Frame.parseVM('#0 Foo (https://dart.dev/thing.dart:5:10)').toString(),
+          equals('https://dart.dev/thing.dart 5:10 in Foo'));
+    });
+
+    test('converts "<anonymous closure>" to "<fn>"', () {
+      expect(
+          Frame.parseVM('#0 Foo.<anonymous closure> '
+                  '(dart:core/uri.dart:5:10)')
+              .toString(),
+          equals('dart:core/uri.dart 5:10 in Foo.<fn>'));
+    });
+
+    test('prints a frame without a column correctly', () {
+      expect(Frame.parseVM('#0 Foo (dart:core/uri.dart:5)').toString(),
+          equals('dart:core/uri.dart 5 in Foo'));
+    });
+
+    test('prints relative paths as relative', () {
+      var relative = path.normalize('relative/path/to/foo.dart');
+      expect(Frame.parseFriendly('$relative 5:10  Foo').toString(),
+          equals('$relative 5:10 in Foo'));
+    });
+  });
+
+  test('parses a V8 Wasm frame with a name', () {
+    var frame = Frame.parseV8('    at Error._throwWithCurrentStackTrace '
+        '(wasm://wasm/0006d966:wasm-function[119]:0xbb13)');
+    expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+    expect(frame.line, 1);
+    expect(frame.column, 0xbb13 + 1);
+    expect(frame.member, 'Error._throwWithCurrentStackTrace');
+  });
+
+  test('parses a V8 Wasm frame with a name with spaces', () {
+    var frame = Frame.parseV8('   at main tear-off trampoline '
+        '(wasm://wasm/0017fbea:wasm-function[863]:0x23cc8)');
+    expect(frame.uri, Uri.parse('wasm://wasm/0017fbea'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x23cc8 + 1);
+    expect(frame.member, 'main tear-off trampoline');
+  });
+
+  test('parses a V8 Wasm frame with a name with colons and parens', () {
+    var frame = Frame.parseV8('   at a::b::c() '
+        '(https://a.b.com/x/y/z.wasm:wasm-function[66334]:0x12c28ad)');
+    expect(frame.uri, Uri.parse('https://a.b.com/x/y/z.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x12c28ad + 1);
+    expect(frame.member, 'a::b::c()');
+  });
+
+  test('parses a V8 Wasm frame without a name', () {
+    var frame =
+        Frame.parseV8('    at wasm://wasm/0006d966:wasm-function[119]:0xbb13');
+    expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+    expect(frame.line, 1);
+    expect(frame.column, 0xbb13 + 1);
+    expect(frame.member, '119');
+  });
+
+  test('parses a Firefox Wasm frame with a name', () {
+    var frame = Frame.parseFirefox(
+        'g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+    expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x143b4 + 1);
+    expect(frame.member, 'g');
+  });
+
+  test('parses a Firefox Wasm frame with a name with spaces', () {
+    var frame = Frame.parseFirefox(
+        'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387');
+    expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x14387 + 1);
+    expect(frame.member, 'main tear-off trampoline');
+  });
+
+  test('parses a Firefox Wasm frame without a name', () {
+    var frame = Frame.parseFirefox(
+        '@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+    expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+    expect(frame.line, 1);
+    expect(frame.column, 0x143b4 + 1);
+    expect(frame.member, '796');
+  });
+
+  test('parses a Safari Wasm frame with a name', () {
+    var frame = Frame.parseSafari('<?>.wasm-function[g]@[wasm code]');
+    expect(frame.uri, Uri.parse('wasm code'));
+    expect(frame.line, null);
+    expect(frame.column, null);
+    expect(frame.member, 'g');
+  });
+
+  test('parses a Safari Wasm frame with a name', () {
+    var frame = Frame.parseSafari(
+        '<?>.wasm-function[main tear-off trampoline]@[wasm code]');
+    expect(frame.uri, Uri.parse('wasm code'));
+    expect(frame.line, null);
+    expect(frame.column, null);
+    expect(frame.member, 'main tear-off trampoline');
+  });
+
+  test('parses a Safari Wasm frame without a name', () {
+    var frame = Frame.parseSafari('<?>.wasm-function[796]@[wasm code]');
+    expect(frame.uri, Uri.parse('wasm code'));
+    expect(frame.line, null);
+    expect(frame.column, null);
+    expect(frame.member, '796');
+  });
+}
+
+void expectIsUnparsed(Frame Function(String) constructor, String text) {
+  var frame = constructor(text);
+  expect(frame, isA<UnparsedFrame>());
+  expect(frame.toString(), equals(text));
+}
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart
new file mode 100644
index 0000000..e09de95
--- /dev/null
+++ b/pkgs/stack_trace/test/trace_test.dart
@@ -0,0 +1,615 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+  // This just shouldn't crash.
+  test('a native stack trace is parseable', Trace.current);
+
+  group('.parse', () {
+    test('.parse parses a V8 stack trace with eval statment correctly', () {
+      var trace = Trace.parse(r'''Error
+    at Object.eval (eval at Foo (main.dart.js:588), <anonymous>:3:47)''');
+      expect(trace.frames[0].uri, Uri.parse('main.dart.js'));
+      expect(trace.frames[0].member, equals('Object.eval'));
+      expect(trace.frames[0].line, equals(588));
+      expect(trace.frames[0].column, isNull);
+    });
+
+    test('.parse parses a VM stack trace correctly', () {
+      var trace = Trace.parse(
+        '#0      Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)\n'
+        '#1      zip.<anonymous closure>.zap (dart:async/future.dart:0:2)\n'
+        '#2      zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)',
+      );
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:async/future.dart')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.dart')));
+    });
+
+    test('parses a V8 stack trace correctly', () {
+      var trace = Trace.parse('Error\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('Exception: foo\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('Exception: foo\n'
+          '    bar\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('Exception: foo\n'
+          '    bar\n'
+          '    at Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '    at https://example.com/stuff.js:0:2\n'
+          '    at (anonymous function).zip.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].member, equals('<fn>'));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+      expect(trace.frames[2].member, equals('<fn>.zip.zap'));
+    });
+
+    // JavaScriptCore traces are just like V8, except that it doesn't have a
+    // header and it starts with a tab rather than spaces.
+    test('parses a JavaScriptCore stack trace correctly', () {
+      var trace =
+          Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+              '\tat https://example.com/stuff.js:0:2\n'
+              '\tat zip.<anonymous>.zap '
+              '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+          '\tat \n'
+          '\tat zip.<anonymous>.zap '
+          '(https://pub.dev/thing.js:1:100)');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[1].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace correctly', () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('zip/<@https://example.com/stuff.js:0\n'
+          'Foo._bar@https://example.com/stuff.js:42\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+      trace = Trace.parse('zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'Foo._bar@https://example.com/stuff.js:42');
+
+      expect(
+          trace.frames[0].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace containing native code correctly',
+        () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+          '[native code]');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+      expect(trace.frames.length, equals(3));
+    });
+
+    test('parses a Firefox/Safari stack trace without a method name correctly',
+        () {
+      var trace = Trace.parse('https://example.com/stuff.js:42\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].member, equals('<fn>'));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace with an empty line correctly',
+        () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+          '\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a Firefox/Safari stack trace with a column number correctly',
+        () {
+      var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42:2\n'
+          'zip/<@https://example.com/stuff.js:0\n'
+          'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(trace.frames[0].line, equals(42));
+      expect(trace.frames[0].column, equals(2));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://example.com/stuff.js')));
+      expect(
+          trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+    });
+
+    test('parses a package:stack_trace stack trace correctly', () {
+      var trace =
+          Trace.parse('https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/baz.dart        Foo.<fn>.bar');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+    });
+
+    test('parses a package:stack_trace stack chain correctly', () {
+      var trace =
+          Trace.parse('https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/baz.dart        Foo.<fn>.bar\n'
+              '===== asynchronous gap ===========================\n'
+              'https://dart.dev/foo/bang.dart 10:11  Foo.<fn>.bar\n'
+              'https://dart.dev/foo/quux.dart        Foo.<fn>.bar');
+
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+      expect(trace.frames[3].uri,
+          equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+    });
+
+    test('parses a package:stack_trace stack chain with end gap correctly', () {
+      var trace = Trace.parse(
+        'https://dart.dev/foo/bar.dart 10:11  Foo.<fn>.bar\n'
+        'https://dart.dev/foo/baz.dart        Foo.<fn>.bar\n'
+        'https://dart.dev/foo/bang.dart 10:11  Foo.<fn>.bar\n'
+        'https://dart.dev/foo/quux.dart        Foo.<fn>.bar===== asynchronous gap ===========================\n',
+      );
+
+      expect(trace.frames.length, 4);
+      expect(trace.frames[0].uri,
+          equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+      expect(trace.frames[1].uri,
+          equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+      expect(trace.frames[2].uri,
+          equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+      expect(trace.frames[3].uri,
+          equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+    });
+
+    test('parses a real package:stack_trace stack trace correctly', () {
+      var traceString = Trace.current().toString();
+      expect(Trace.parse(traceString).toString(), equals(traceString));
+    });
+
+    test('parses an empty string correctly', () {
+      var trace = Trace.parse('');
+      expect(trace.frames, isEmpty);
+      expect(trace.toString(), equals(''));
+    });
+
+    test('parses trace with async gap correctly', () {
+      var trace = Trace.parse('#0      bop (file:///pull.dart:42:23)\n'
+          '<asynchronous suspension>\n'
+          '#1      twist (dart:the/future.dart:0:2)\n'
+          '#2      main (dart:my/file.dart:4:6)\n');
+
+      expect(trace.frames.length, 3);
+      expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+      expect(trace.frames[2].uri, equals(Uri.parse('dart:my/file.dart')));
+    });
+
+    test('parses trace with async gap at end correctly', () {
+      var trace = Trace.parse('#0      bop (file:///pull.dart:42:23)\n'
+          '#1      twist (dart:the/future.dart:0:2)\n'
+          '<asynchronous suspension>\n');
+
+      expect(trace.frames.length, 2);
+      expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+      expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+    });
+
+    test('parses a V8 stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          '\tat Error._throwWithCurrentStackTrace (wasm://wasm/0006d892:wasm-function[119]:0xbaf8)\n'
+          '\tat main (wasm://wasm/0006d892:wasm-function[792]:0x14378)\n'
+          '\tat main tear-off trampoline (wasm://wasm/0006d892:wasm-function[794]:0x14387)\n'
+          '\tat _invokeMain (wasm://wasm/0006d892:wasm-function[70]:0xa56c)\n'
+          '\tat InstantiatedApp.invokeMain (/home/user/test.mjs:361:37)\n'
+          '\tat main (/home/user/run_wasm.js:416:21)\n'
+          '\tat async action (/home/user/run_wasm.js:353:38)\n'
+          '\tat async eventLoop (/home/user/run_wasm.js:329:9)');
+
+      expect(trace.frames.length, 8);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('wasm://wasm/0006d892'));
+      expect(trace.frames[0].line, 1);
+      expect(trace.frames[0].column, 0xbaf8 + 1);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[4].line, 361);
+      expect(trace.frames[4].column, 37);
+      expect(trace.frames[4].member, 'InstantiatedApp.invokeMain');
+
+      expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+      expect(trace.frames[5].line, 416);
+      expect(trace.frames[5].column, 21);
+      expect(trace.frames[5].member, 'main');
+    });
+
+    test('parses Firefox stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          'Error._throwWithCurrentStackTrace@http://localhost:8080/test.wasm:wasm-function[119]:0xbaf8\n'
+          'main@http://localhost:8080/test.wasm:wasm-function[792]:0x14378\n'
+          'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387\n'
+          '_invokeMain@http://localhost:8080/test.wasm:wasm-function[70]:0xa56c\n'
+          'invoke@http://localhost:8080/test.mjs:48:26');
+
+      expect(trace.frames.length, 5);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('http://localhost:8080/test.wasm'));
+      expect(trace.frames[0].line, 1);
+      expect(trace.frames[0].column, 0xbaf8 + 1);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('http://localhost:8080/test.mjs'));
+      expect(trace.frames[4].line, 48);
+      expect(trace.frames[4].column, 26);
+      expect(trace.frames[4].member, 'invoke');
+    });
+
+    test('parses JSShell stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          'Error._throwWithCurrentStackTrace@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[119]:0xbaf8\n'
+          'main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378\n'
+          'main tear-off trampoline@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[794]:0x14387\n'
+          '_invokeMain@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[70]:0xa56c\n'
+          'invokeMain@/home/user/test.mjs:361:37\n'
+          'main@/home/user/run_wasm.js:416:21\n'
+          'async*action@/home/user/run_wasm.js:353:44\n'
+          'eventLoop@/home/user/run_wasm.js:329:15\n'
+          'self.dartMainRunner@/home/user/run_wasm.js:354:14\n'
+          '@/home/user/run_wasm.js:419:15');
+
+      expect(trace.frames.length, 10);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[0].line, 1);
+      expect(trace.frames[0].column, 0xbaf8 + 1);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[4].line, 361);
+      expect(trace.frames[4].column, 37);
+      expect(trace.frames[4].member, 'invokeMain');
+
+      expect(trace.frames[9].uri, Uri.parse('file:///home/user/run_wasm.js'));
+      expect(trace.frames[9].line, 419);
+      expect(trace.frames[9].column, 15);
+      expect(trace.frames[9].member, '<fn>');
+    });
+
+    test('parses Safari stack frace with Wasm frames correctly', () {
+      var trace = Trace.parse(
+          '<?>.wasm-function[Error._throwWithCurrentStackTrace]@[wasm code]\n'
+          '<?>.wasm-function[main]@[wasm code]\n'
+          '<?>.wasm-function[main tear-off trampoline]@[wasm code]\n'
+          '<?>.wasm-function[_invokeMain]@[wasm code]\n'
+          'invokeMain@/home/user/test.mjs:361:48\n'
+          '@/home/user/run_wasm.js:416:31');
+
+      expect(trace.frames.length, 6);
+
+      for (final frame in trace.frames) {
+        expect(frame is UnparsedFrame, false);
+      }
+
+      expect(trace.frames[0].uri, Uri.parse('wasm code'));
+      expect(trace.frames[0].line, null);
+      expect(trace.frames[0].column, null);
+      expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+      expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+      expect(trace.frames[4].line, 361);
+      expect(trace.frames[4].column, 48);
+      expect(trace.frames[4].member, 'invokeMain');
+
+      expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+      expect(trace.frames[5].line, 416);
+      expect(trace.frames[5].column, 31);
+      expect(trace.frames[5].member, '<fn>');
+    });
+  });
+
+  test('.toString() nicely formats the stack trace', () {
+    var trace = Trace.parse('''
+#0      Foo._bar (foo/bar.dart:42:21)
+#1      zip.<anonymous closure>.zap (dart:async/future.dart:0:2)
+#2      zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)
+''');
+
+    expect(trace.toString(), equals('''
+${path.join('foo', 'bar.dart')} 42:21                Foo._bar
+dart:async/future.dart 0:2        zip.<fn>.zap
+https://pub.dev/thing.dart 1:100  zip.<fn>.zap
+'''));
+  });
+
+  test('.vmTrace returns a native-style trace', () {
+    var uri = path.toUri(path.absolute('foo'));
+    var trace = Trace([
+      Frame(uri, 10, 20, 'Foo.<fn>'),
+      Frame(Uri.parse('https://dart.dev/foo.dart'), null, null, 'bar'),
+      Frame(Uri.parse('dart:async'), 15, null, 'baz'),
+    ]);
+
+    expect(
+        trace.vmTrace.toString(),
+        equals('#1      Foo.<anonymous closure> ($uri:10:20)\n'
+            '#2      bar (https://dart.dev/foo.dart:0:0)\n'
+            '#3      baz (dart:async:15:0)\n'));
+  });
+
+  group('folding', () {
+    group('.terse', () {
+      test('folds core frames together bottom-up', () {
+        var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#0 notCore (foo.dart:42:21)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+#5 alsoNotCore (bar.dart:10:20)
+''');
+
+        expect(trace.terse.toString(), equals('''
+dart:core       bottom
+foo.dart 42:21  notCore
+dart:async      bottom
+bar.dart 10:20  alsoNotCore
+'''));
+      });
+
+      test('folds empty async frames', () {
+        var trace = Trace.parse('''
+#0 top (dart:async/future.dart:0:2)
+#1 empty.<<anonymous closure>_async_body> (bar.dart)
+#2 bottom (dart:async-patch/future.dart:9:11)
+#3 notCore (foo.dart:42:21)
+''');
+
+        expect(trace.terse.toString(), equals('''
+dart:async      bottom
+foo.dart 42:21  notCore
+'''));
+      });
+
+      test('removes the bottom-most async frame', () {
+        var trace = Trace.parse('''
+#0 notCore (foo.dart:42:21)
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+''');
+
+        expect(trace.terse.toString(), equals('''
+foo.dart 42:21  notCore
+'''));
+      });
+
+      test("won't make a trace empty", () {
+        var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+''');
+
+        expect(trace.terse.toString(), equals('''
+dart:core  bottom
+'''));
+      });
+
+      test("won't panic on an empty trace", () {
+        expect(Trace.parse('').terse.toString(), equals(''));
+      });
+    });
+
+    group('.foldFrames', () {
+      test('folds frames together bottom-up', () {
+        var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (foo.dart:1:100)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (dart:io/socket.dart:5:10)
+#5 fooBottom (dart:async-patch/future.dart:9:11)
+''');
+
+        var folded =
+            trace.foldFrames((frame) => frame.member!.startsWith('foo'));
+        expect(folded.toString(), equals('''
+foo.dart 42:21                     notFoo
+foo.dart 1:100                     fooBottom
+bar.dart 10:20                     alsoNotFoo
+dart:async-patch/future.dart 9:11  fooBottom
+'''));
+      });
+
+      test('will never fold unparsed frames', () {
+        var trace = Trace.parse(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+''');
+
+        expect(trace.foldFrames((frame) => true).toString(), equals(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+'''));
+      });
+
+      group('with terse: true', () {
+        test('folds core frames as well', () {
+          var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 coreBottom (dart:async/future.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 coreBottom (dart:async-patch/future.dart:9:11)
+''');
+
+          var folded = trace.foldFrames(
+              (frame) => frame.member!.startsWith('foo'),
+              terse: true);
+          expect(folded.toString(), equals('''
+foo.dart 42:21  notFoo
+dart:async      coreBottom
+bar.dart 10:20  alsoNotFoo
+'''));
+        });
+
+        test('shortens folded frames', () {
+          var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (package:foo/bar.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 fooBottom (foo/bar.dart:9:11)
+#6 againNotFoo (bar.dart:20:20)
+''');
+
+          var folded = trace.foldFrames(
+              (frame) => frame.member!.startsWith('foo'),
+              terse: true);
+          expect(folded.toString(), equals('''
+foo.dart 42:21  notFoo
+package:foo     fooBottom
+bar.dart 10:20  alsoNotFoo
+foo             fooBottom
+bar.dart 20:20  againNotFoo
+'''));
+        });
+
+        test('removes the bottom-most folded frame', () {
+          var trace = Trace.parse('''
+#2 fooTop (package:foo/bar.dart:0:2)
+#3 notFoo (bar.dart:10:20)
+#5 fooBottom (foo/bar.dart:9:11)
+''');
+
+          var folded = trace.foldFrames(
+              (frame) => frame.member!.startsWith('foo'),
+              terse: true);
+          expect(folded.toString(), equals('''
+package:foo     fooTop
+bar.dart 10:20  notFoo
+'''));
+        });
+      });
+    });
+  });
+}
diff --git a/pkgs/stack_trace/test/utils.dart b/pkgs/stack_trace/test/utils.dart
new file mode 100644
index 0000000..98cb5ed
--- /dev/null
+++ b/pkgs/stack_trace/test/utils.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `member` field.
+Matcher frameMember(Object? matcher) =>
+    isA<Frame>().having((p0) => p0.member, 'member', matcher);
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `library` field.
+Matcher frameLibrary(Object? matcher) =>
+    isA<Frame>().having((p0) => p0.library, 'library', matcher);
diff --git a/pkgs/stack_trace/test/vm_test.dart b/pkgs/stack_trace/test/vm_test.dart
new file mode 100644
index 0000000..70ac014
--- /dev/null
+++ b/pkgs/stack_trace/test/vm_test.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// This file tests stack_trace's ability to parse live stack traces. It's a
+/// dual of dartium_test.dart, since method names can differ somewhat from
+/// platform to platform. No similar file exists for dart2js since the specific
+/// method names there are implementation details.
+@TestOn('vm')
+library;
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+// The name of this (trivial) function is verified as part of the test
+String getStackTraceString() => StackTrace.current.toString();
+
+// The name of this (trivial) function is verified as part of the test
+StackTrace getStackTraceObject() => StackTrace.current;
+
+Frame getCaller([int? level]) {
+  if (level == null) return Frame.caller();
+  return Frame.caller(level);
+}
+
+Frame nestedGetCaller(int level) => getCaller(level);
+
+Trace getCurrentTrace([int level = 0]) => Trace.current(level);
+
+Trace nestedGetCurrentTrace(int level) => getCurrentTrace(level);
+
+void main() {
+  group('Trace', () {
+    test('.parse parses a real stack trace correctly', () {
+      var string = getStackTraceString();
+      var trace = Trace.parse(string);
+      expect(path.url.basename(trace.frames.first.uri.path),
+          equals('vm_test.dart'));
+      expect(trace.frames.first.member, equals('getStackTraceString'));
+    });
+
+    test('converts from a native stack trace correctly', () {
+      var trace = Trace.from(getStackTraceObject());
+      expect(path.url.basename(trace.frames.first.uri.path),
+          equals('vm_test.dart'));
+      expect(trace.frames.first.member, equals('getStackTraceObject'));
+    });
+
+    test('.from handles a stack overflow trace correctly', () {
+      void overflow() => overflow();
+
+      late Trace? trace;
+      try {
+        overflow();
+      } catch (_, stackTrace) {
+        trace = Trace.from(stackTrace);
+      }
+
+      expect(trace!.frames.first.member, equals('main.<fn>.<fn>.overflow'));
+    });
+
+    group('.current()', () {
+      test('with no argument returns a trace starting at the current frame',
+          () {
+        var trace = Trace.current();
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('at level 0 returns a trace starting at the current frame', () {
+        var trace = Trace.current();
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('at level 1 returns a trace starting at the parent frame', () {
+        var trace = getCurrentTrace(1);
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('at level 2 returns a trace starting at the grandparent frame', () {
+        var trace = nestedGetCurrentTrace(2);
+        expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+      });
+
+      test('throws an ArgumentError for negative levels', () {
+        expect(() => Trace.current(-1), throwsArgumentError);
+      });
+    });
+  });
+
+  group('Frame.caller()', () {
+    test('with no argument returns the parent frame', () {
+      expect(getCaller().member, equals('main.<fn>.<fn>'));
+    });
+
+    test('at level 0 returns the current frame', () {
+      expect(getCaller(0).member, equals('getCaller'));
+    });
+
+    test('at level 1 returns the current frame', () {
+      expect(getCaller(1).member, equals('main.<fn>.<fn>'));
+    });
+
+    test('at level 2 returns the grandparent frame', () {
+      expect(nestedGetCaller(2).member, equals('main.<fn>.<fn>'));
+    });
+
+    test('throws an ArgumentError for negative levels', () {
+      expect(() => Frame.caller(-1), throwsArgumentError);
+    });
+  });
+}