main 3fd3e105c4a1 cached
80 files
193.3 KB
55.2k tokens
274 symbols
1 requests
Download .txt
Showing preview only (213K chars total). Download the full file or copy to clipboard to get everything.
Repository: elixir-error-tracker/error-tracker
Branch: main
Commit: 3fd3e105c4a1
Files: 80
Total size: 193.3 KB

Directory structure:
gitextract_9aka67fi/

├── .formatter.exs
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── elixir.yml
├── .gitignore
├── .tool-versions
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets/
│   ├── bun.lockb
│   ├── css/
│   │   └── app.css
│   ├── js/
│   │   └── app.js
│   ├── package.json
│   └── tailwind.config.js
├── config/
│   ├── config.exs
│   ├── dev.example.exs
│   └── test.example.exs
├── dev.exs
├── guides/
│   └── Getting Started.md
├── lib/
│   ├── error_tracker/
│   │   ├── application.ex
│   │   ├── filter.ex
│   │   ├── ignorer.ex
│   │   ├── integrations/
│   │   │   ├── oban.ex
│   │   │   ├── phoenix.ex
│   │   │   └── plug.ex
│   │   ├── migration/
│   │   │   ├── mysql/
│   │   │   │   ├── v03.ex
│   │   │   │   ├── v04.ex
│   │   │   │   └── v05.ex
│   │   │   ├── mysql.ex
│   │   │   ├── postgres/
│   │   │   │   ├── v01.ex
│   │   │   │   ├── v02.ex
│   │   │   │   ├── v03.ex
│   │   │   │   ├── v04.ex
│   │   │   │   └── v05.ex
│   │   │   ├── postgres.ex
│   │   │   ├── sql_migrator.ex
│   │   │   ├── sqlite/
│   │   │   │   ├── v02.ex
│   │   │   │   ├── v03.ex
│   │   │   │   ├── v04.ex
│   │   │   │   └── v05.ex
│   │   │   └── sqlite.ex
│   │   ├── migration.ex
│   │   ├── plugins/
│   │   │   └── pruner.ex
│   │   ├── repo.ex
│   │   ├── schemas/
│   │   │   ├── error.ex
│   │   │   ├── occurrence.ex
│   │   │   └── stacktrace.ex
│   │   ├── telemetry.ex
│   │   ├── web/
│   │   │   ├── components/
│   │   │   │   ├── core_components.ex
│   │   │   │   ├── layouts/
│   │   │   │   │   ├── live.html.heex
│   │   │   │   │   └── root.html.heex
│   │   │   │   └── layouts.ex
│   │   │   ├── helpers.ex
│   │   │   ├── hooks/
│   │   │   │   └── set_assigns.ex
│   │   │   ├── live/
│   │   │   │   ├── dashboard.ex
│   │   │   │   ├── dashboard.html.heex
│   │   │   │   ├── show.ex
│   │   │   │   └── show.html.heex
│   │   │   ├── router/
│   │   │   │   └── routes.ex
│   │   │   ├── router.ex
│   │   │   └── search.ex
│   │   └── web.ex
│   ├── error_tracker.ex
│   └── mix/
│       └── tasks/
│           └── error_tracker.install.ex
├── mix.exs
├── priv/
│   ├── repo/
│   │   ├── migrations/
│   │   │   └── 20240527155639_create_error_tracker_tables.exs
│   │   └── seeds.exs
│   └── static/
│       ├── app.css
│       └── app.js
└── test/
    ├── error_tracker/
    │   ├── filter_test.exs
    │   ├── ignorer_test.exs
    │   ├── schemas/
    │   │   └── occurrence_test.exs
    │   └── telemetry_test.exs
    ├── error_tracker_test.exs
    ├── integrations/
    │   └── plug_test.exs
    ├── support/
    │   ├── case.ex
    │   ├── lite_repo.ex
    │   ├── mysql_repo.ex
    │   └── repo.ex
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .formatter.exs
================================================
# Used by "mix format"
locals_without_parens = [error_tracker_dashboard: 1, error_tracker_dashboard: 2]

# Parse SemVer minor elixir version from project configuration
# eg `"~> 1.15"` version requirement will yield `"1.15"`
[elixir_minor_version | _] = Regex.run(~r/([\d\.]+)/, Mix.Project.config()[:elixir])

[
  import_deps: [:ecto, :ecto_sql, :plug, :phoenix],
  inputs: ["{mix,.formatter,dev,dev.*}.exs", "{config,lib,test}/**/*.{heex,ex,exs}"],
  plugins: [Phoenix.LiveView.HTMLFormatter, Styler],
  locals_without_parens: locals_without_parens,
  export: [locals_without_parens: locals_without_parens],
  styler: [
    minimum_supported_elixir_version: "#{elixir_minor_version}.0"
  ]
]


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is. Remember that we don't provide support to third-party libraries such as Tower.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/elixir.yml
================================================
name: CI
on:
  push:
    branches:
      - main
    paths-ignore:
      - 'guides/**'
  pull_request:
    paths-ignore:
      - 'guides/**'
env:
  MIX_ENV: test

jobs:
  code_quality_and_tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - elixir: "1.15.x"
            erlang: "24.x"
          - elixir: "1.16.x"
            erlang: "24.x"
          - elixir: "1.17.x"
            erlang: "27.x"
          - elixir: "1.18.x"
            erlang: "27.x"
          - elixir: "1.19.x"
            erlang: "28.x"
          - elixir: "latest"
            erlang: "28.x"
    services:
      db:
        image: postgres:15
        ports: ["5432:5432"]
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      mariadb:
        image: mariadb:11
        ports: ["3306:3306"]
        env:
          MARIADB_ROOT_PASSWORD: root
        options: >-
          --health-cmd "healthcheck.sh --connect --innodb_initialized"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    name: Elixir v${{ matrix.elixir }}, Erlang v${{ matrix.erlang }}
    steps:
      - uses: actions/checkout@v4

      - name: Generate test configuration
        run: cp config/test.example.exs config/test.exs

      - uses: erlef/setup-beam@v1
        with:
          otp-version: ${{ matrix.erlang }}
          elixir-version: ${{ matrix.elixir }}

      - name: Retrieve Dependencies Cache
        uses: actions/cache@v4
        id: mix-cache
        with:
          path: |
            deps
            _build
          key: ${{ runner.os }}-${{ matrix.erlang }}-${{ matrix.elixir }}-mix-${{ hashFiles('**/mix.lock') }}

      - name: Install Mix Dependencies
        run: mix deps.get

      - name: Check unused dependencies
        run: mix deps.unlock --check-unused

      - name: Compile dependencies
        run: mix deps.compile

      - name: Check format
        run: mix format --check-formatted

      - name: Check application compile warnings
        run: mix compile --force --warnings-as-errors

      - name: Run Tests - SQLite3
        run: mix test --warnings-as-errors
        env:
          DB: sqlite

      - name: Run Tests - PostgreSQL
        run: mix test --warnings-as-errors
        env:
          DB: postgres

      - name: Run Tests - MySQL/MariaDB
        run: mix test --warnings-as-errors
        env:
          DB: mysql


================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
error_tracker-*.tar

# Temporary files, for example, from tests.
/tmp/

# Configuration files (only the examples are committed)
/config/dev.exs
/config/test.exs

# Assets
/assets/node_modules

# SQLite3 databases
*.db
*.db-shm
*.db-wal


================================================
FILE: .tool-versions
================================================
elixir 1.19
erlang 28.1


================================================
FILE: CHANGELOG.md
================================================
# Changelog

Please see [our GitHub "Releases" page](https://github.com/elixir-error-tracker/error-tracker/releases).


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [2024] [elixir-error-tracker]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# 🐛 ErrorTracker

<a title="GitHub CI" href="https://github.com/elixir-error-tracker/error-tracker/actions"><img src="https://github.com/elixir-error-tracker/error-tracker/workflows/CI/badge.svg" alt="GitHub CI" /></a>
<a title="Latest release" href="https://hex.pm/packages/error_tracker"><img src="https://img.shields.io/hexpm/v/error_tracker.svg" alt="Latest release" /></a>
<a title="View documentation" href="https://hexdocs.pm/error_tracker"><img src="https://img.shields.io/badge/hex.pm-docs-blue.svg" alt="View documentation" /></a>

**An Elixir based built-in error tracking solution.**

ErrorTracker captures errors in your application and stores them in the database. It also provides a web dashboard from where you can find, inspect and resolve captured errors.

**Does it send notifications or integrate with issue trackers?**

ErrorTrackers's goal is to track errors. Period. It provides a nice Telemetry integration that you can attach to and use to send notifications, open tickets in your issue tracker and whatnot.

**Why another error tracker?**

While there are multiple SaaS error trackers available, this is the only Elixir-native built-in error tracker that runs as part of your application. It gives you full control over where, how and what data is stored so it is always on your control and doesn't leave your system.\
You can see a more detailed explanation [here](https://crbelaus.com/2024/07/31/built-in-elixir-error-reporting-tracking).

<a href="guides/screenshots/error-dashboard.png">
  <img src="guides/screenshots/error-dashboard.png" alt="ErrorTracker web dashboard" width="400">
</a>
<a href="guides/screenshots/error-detail.png">
  <img src="guides/screenshots/error-detail.png" alt="ErrorTracker error detail" width="400">
</a>

## Configuration

Take a look at the [Getting Started](/guides/Getting%20Started.md) guide.

## Development

### Initial setup and dependencies

If this is the first time that you set up this project you will to generate the configuration files and adapt their content to your local environment:

```
cp config/dev.example.exs config/dev.exs
cp config/test.example.exs config/test.exs
```

Then, you will need to download the dependencies:

```
mix deps.get
```

### Assets

In order to participate in the development of this project, you may need to know how to compile the assets needed to use the Web UI.

To do so, you need to first make a clean build:

```
mix do assets.install, assets.build
```

That task will build the JS and CSS of the project.

The JS is not expected to change too much because we rely in LiveView, but if
you make any change just execute that command again and you are good to go.

In the case of CSS, as it is automatically generated by Tailwind, you need to
start the watcher when your intention is to modify the classes used.

To do so you can execute this task in a separate terminal:

```
mix assets.watch
```



### Development server

We have a `dev.exs` script based on [Phoenix Playground](https://github.com/phoenix-playground/phoenix_playground) that starts a development server.

```
iex dev.exs
```


================================================
FILE: assets/css/app.css
================================================
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

::-webkit-scrollbar {
  height: 6px;
  width: 6px;
  @apply bg-gray-300;
}

::-webkit-scrollbar-thumb {
  @apply bg-gray-500;
  border-radius: 4px;
}


================================================
FILE: assets/js/app.js
================================================
// Phoenix assets are imported from dependencies.
import topbar from "topbar";

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
let livePath = document.querySelector("meta[name='live-path']").getAttribute("content");
let liveTransport = document .querySelector("meta[name='live-transport']") .getAttribute("content");

const Hooks = {
  JsonPrettyPrint: {
    mounted() {
      this.formatJson();
    },
    updated() {
      this.formatJson();
    },
    formatJson() {
      try {
        // Get the raw JSON content
        const rawJson = this.el.textContent.trim();
        // Parse and stringify with indentation
        const formattedJson = JSON.stringify(JSON.parse(rawJson), null, 2);
        // Update the element content
        this.el.textContent = formattedJson;
      } catch (error) {
        console.error("Error formatting JSON:", error);
        // Keep the original content if there's an error
      }
    }
  }
};

let liveSocket = new LiveView.LiveSocket(livePath, Phoenix.Socket, {
  transport: liveTransport === "longpoll" ? Phoenix.LongPoll : WebSocket,
  params: { _csrf_token: csrfToken },
  hooks: Hooks

});

// Show progress bar on live navigation and form submits
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());

// connect if there are any LiveViews on the page
liveSocket.connect();
window.liveSocket = liveSocket;


================================================
FILE: assets/package.json
================================================
{
  "workspaces": [
    "../deps/*"
  ],
  "dependencies": {
    "phoenix": "workspace:*",
    "phoenix_live_view": "workspace:*",
    "topbar": "^3.0.0"
  }
}


================================================
FILE: assets/tailwind.config.js
================================================
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration

let plugin = require('tailwindcss/plugin')

module.exports = {
  content: [
    './js/**/*.js',
    '../lib/error_tracker/web.ex',
    '../lib/error_tracker/web/**/*.*ex'
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
    plugin(({addVariant}) => addVariant('phx-no-feedback', ['&.phx-no-feedback', '.phx-no-feedback &'])),
    plugin(({addVariant}) => addVariant('phx-click-loading', ['&.phx-click-loading', '.phx-click-loading &'])),
    plugin(({addVariant}) => addVariant('phx-submit-loading', ['&.phx-submit-loading', '.phx-submit-loading &'])),
    plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &']))
  ]
}


================================================
FILE: config/config.exs
================================================
import Config

import_config "#{config_env()}.exs"


================================================
FILE: config/dev.example.exs
================================================
import Config

config :bun,
  version: "1.1.18",
  default: [
    args: ~w(build app.js --outdir=../../priv/static),
    cd: Path.expand("../assets/js", __DIR__),
    env: %{}
  ]

# MySQL/MariaDB adapter
#
# To use MySQL/MariaDB on your local development machine uncomment these lines and
# comment the lines of other adapters.
#
# config :error_tracker, :ecto_adapter, :mysql
#
# config :error_tracker, ErrorTrackerDev.Repo,
#   url: "ecto://root:root@127.0.0.1/error_tracker_dev"
#
# SQLite3 adapter
#
# To use SQLite3 on your local development machine uncomment these lines and
# comment the lines of other adapters.
#
# config :error_tracker, :ecto_adapter, :sqlite3
#
# config :error_tracker, ErrorTrackerDev.Repo,
#   database: System.get_env("SQLITE_DB") || "dev.db"
#
# PostgreSQL adapter
#
# To use PostgreSQL on your local development machine uncomment these lines and
# comment the lines of other adapters.
config :error_tracker, ErrorTrackerDev.Repo, url: "ecto://postgres:postgres@127.0.0.1/error_tracker_dev"
config :error_tracker, :ecto_adapter, :postgres

config :tailwind,
  version: "3.4.3",
  default: [
    args: ~w(
      --config=tailwind.config.js
      --input=css/app.css
      --output=../priv/static/app.css
      ),
    cd: Path.expand("../assets", __DIR__)
  ]


================================================
FILE: config/test.example.exs
================================================
import Config

alias Ecto.Adapters.SQL.Sandbox
alias ErrorTracker.Test.Repo

config :error_tracker, ErrorTracker.Test.LiteRepo,
  database: "priv/lite_repo/test.db",
  pool: Sandbox,
  log: false,
  # Use the same migrations as the PostgreSQL repo
  priv: "priv/repo"

config :error_tracker, ErrorTracker.Test.MySQLRepo,
  url: "ecto://root:root@127.0.0.1/error_tracker_test",
  pool: Sandbox,
  log: false,
  # Use the same migrations as the PostgreSQL repo
  priv: "priv/repo"

config :error_tracker, Repo,
  url: "ecto://postgres:postgres@127.0.0.1/error_tracker_test",
  pool: Sandbox,
  log: false

config :error_tracker, ecto_repos: [Repo]

# Repo is selected in the test_helper.exs based on the given ENV vars
config :error_tracker, otp_app: :error_tracker


================================================
FILE: dev.exs
================================================
# This is the development server for Errortracker built on the PhoenixLiveDashboard project.
# To start the development server run:
#     $ iex dev.exs
#
Mix.install([
  {:ecto_sqlite3, ">= 0.0.0"},
  {:error_tracker, path: ".", force: true},
  {:phoenix_playground, "~> 0.1.8"}
])

otp_app = :error_tracker_dev

Application.put_all_env(
  error_tracker_dev: [
    {ErrorTrackerDev.Repo, [database: "priv/repo/dev.db"]}
  ],
  error_tracker: [
    {:application, otp_app},
    {:otp_app, otp_app},
    {:repo, ErrorTrackerDev.Repo}
  ]
)

defmodule ErrorTrackerDev.Repo do
  use Ecto.Repo, otp_app: otp_app, adapter: Ecto.Adapters.SQLite3

  require Logger

  defmodule Migration do
    @moduledoc false
    use Ecto.Migration

    def up, do: ErrorTracker.Migration.up()
    def down, do: ErrorTracker.Migration.down()
  end

  def migrate do
    Ecto.Migrator.run(__MODULE__, [{0, __MODULE__.Migration}], :up, all: true)
  end
end

defmodule ErrorTrackerDev.Controller do
  use Phoenix.Controller, formats: [:html]
  use Phoenix.Component

  plug :put_layout, false
  plug :put_view, __MODULE__

  def index(conn, _params) do
    render(conn)
  end

  def index(assigns) do
    ~H"""
    <h2>ErrorTracker Dev server</h2>

    <ul>
      <li></li>
    </ul>
    """
  end

  def noroute(conn, _params) do
    ErrorTracker.add_breadcrumb("ErrorTrackerDev.Controller.noroute/2")

    raise Phoenix.Router.NoRouteError, conn: conn, router: ErrorTrackerDev.Router
  end

  def exception(_conn, _params) do
    ErrorTracker.add_breadcrumb("ErrorTrackerDev.Controller.exception/2")

    raise ErrorTrackerDev.Exception,
      message: "This is a controller exception",
      bread_crumbs: ["First", "Second"]
  end

  def exit(_conn, _params) do
    ErrorTracker.add_breadcrumb("ErrorTrackerDev.Controller.exit/2")

    exit(:timeout)
  end
end

defmodule ErrorTrackerDev.Live do
  @moduledoc false
  use Phoenix.LiveView

  def mount(params, _session, socket) do
    if params["crash_on_mount"] do
      raise("Crashed on mount/3")
    end

    {:ok, socket}
  end

  def handle_params(params, _uri, socket) do
    if params["crash_on_handle_params"] do
      raise "Crashed on handle_params/3"
    end

    {:noreply, socket}
  end

  def handle_event("crash_on_handle_event", _params, _socket) do
    raise "Crashed on handle_event/3"
  end

  def handle_event("crash_on_render", _params, socket) do
    {:noreply, assign(socket, crash_on_render: true)}
  end

  def handle_event("genserver-timeout", _params, socket) do
    GenServer.call(ErrorTrackerDev.GenServer, :timeout, 2000)
    {:noreply, socket}
  end

  def render(assigns) do
    if Map.has_key?(assigns, :crash_on_render) do
      raise "Crashed on render/1"
    end

    ~H"""
    <h1>ErrorTracker Dev server</h1>

    <.link href="/dev/errors" target="_blank">Open the ErrorTracker dashboard</.link>

    <p>
      Errors are stored in the <code>priv/repo/dev.db</code>
      database, which is automatically created by this script.<br />
      If you want to clear the state stop the script, run the following command and start it again. <pre>rm priv/repo/dev.db priv/repo/dev.db-shm priv/repo/dev.db-wal</pre>
    </p>

    <h2>LiveView examples</h2>

    <ul>
      <li>
        <.link href="/?crash_on_mount">Crash on mount/3</.link>
      </li>
      <li>
        <.link patch="/?crash_on_handle_params">Crash on handle_params/3</.link>
      </li>
      <li>
        <.link phx-click="crash_on_render">Crash on render/1</.link>
      </li>
      <li>
        <.link phx-click="crash_on_handle_event">Crash on handle_event/3</.link>
      </li>
      <li>
        <.link phx-click="genserver-timeout">Crash with a GenServer timeout</.link>
      </li>
    </ul>

    <h2>Controller examples</h2>

    <ul>
      <li>
        <.link href="/noroute">Generate a 404 error from the controller</.link>
      </li>
      <li>
        <.link href="/exception">Generate an exception from the controller</.link>
      </li>
      <li>
        <.link href="/plug_exception">Generate an exception from the router</.link>
      </li>
      <li>
        <.link href="/exit">Generate an exit from the controller</.link>
      </li>
    </ul>
    """
  end
end

defmodule ErrorTrackerDev.Router do
  use Phoenix.Router
  use ErrorTracker.Web, :router

  import Phoenix.LiveView.Router

  pipeline :browser do
    plug :accepts, [:html]
    plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
    plug :put_secure_browser_headers
  end

  scope "/" do
    pipe_through :browser

    live "/", ErrorTrackerDev.Live
    get "/noroute", ErrorTrackerDev.Controller, :noroute
    get "/exception", ErrorTrackerDev.Controller, :exception
    get "/exit", ErrorTrackerDev.Controller, :exit

    scope "/dev" do
      error_tracker_dashboard "/errors", csp_nonce_assign_key: :custom_csp_nonce
    end
  end
end

defmodule ErrorTrackerDev.Endpoint do
  use Phoenix.Endpoint, otp_app: :phoenix_playground
  use ErrorTracker.Integrations.Plug

  # Default PhoenixPlayground.Endpoint
  plug Plug.Logger
  socket "/live", Phoenix.LiveView.Socket
  plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
  plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"
  socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
  plug Phoenix.LiveReloader
  plug Phoenix.CodeReloader, reloader: &PhoenixPlayground.CodeReloader.reload/2

  # Use a custom Content Security Policy
  plug :set_csp
  # Raise an exception in the /plug_exception path
  plug :plug_exception
  # Our custom router which allows us to have regular controllers and live views
  plug ErrorTrackerDev.Router

  defp set_csp(conn, _opts) do
    nonce = 10 |> :crypto.strong_rand_bytes() |> Base.encode64()

    policies = [
      "script-src 'self' 'nonce-#{nonce}';",
      "style-src 'self' 'nonce-#{nonce}';"
    ]

    conn
    |> Plug.Conn.assign(:custom_csp_nonce, "#{nonce}")
    |> Plug.Conn.put_resp_header("content-security-policy", Enum.join(policies, " "))
  end

  defp plug_exception(%Plug.Conn{path_info: path_info} = conn, _opts) when is_list(path_info) do
    if "plug_exception" in path_info,
      do: raise("Crashed in Endpoint"),
      else: conn
  end
end

defmodule ErrorTrackerDev.ErrorView do
  def render("404.html", _assigns) do
    "This is a 404"
  end

  def render("500.html", _assigns) do
    "This is a 500"
  end
end

defmodule ErrorTrackerDev.GenServer do
  @moduledoc false
  use GenServer

  # Client

  def start_link(_) do
    GenServer.start_link(__MODULE__, %{})
  end

  # Server (callbacks)

  @impl true
  def init(initial_state) do
    {:ok, initial_state}
  end

  @impl true
  def handle_call(:timeout, _from, state) do
    :timer.sleep(5000)
    {:reply, state, state}
  end
end

defmodule ErrorTrackerDev.Exception do
  @moduledoc false
  defexception [:message, :bread_crumbs]
end

defmodule ErrorTrackerDev.Telemetry do
  @moduledoc false
  def handle_event(event, measure, metadata, _opts) do
    dbg([event, measure, metadata])
  end
end

PhoenixPlayground.start(
  endpoint: ErrorTrackerDev.Endpoint,
  child_specs: [
    {ErrorTrackerDev.Repo, []},
    {ErrorTrackerDev.GenServer, [name: ErrorTrackerDev.GenServer]}
  ],
  open_browser: false,
  debug_errors: false
)

ErrorTrackerDev.Repo.migrate()

:telemetry.attach_many(
  "error-tracker-events",
  [
    [:error_tracker, :error, :new],
    [:error_tracker, :error, :resolved],
    [:error_tracker, :error, :unresolved],
    [:error_tracker, :occurrence, :new]
  ],
  &ErrorTrackerDev.Telemetry.handle_event/4,
  []
)


================================================
FILE: guides/Getting Started.md
================================================
# Getting Started

This guide is an introduction to ErrorTracker, an Elixir-based built-in error tracking solution. ErrorTracker provides a basic and free error-tracking solution integrated in your own application. It is designed to be easy to install and easy to use so you can integrate it in your existing project with minimal changes. The only requirement is a relational database in which errors will be tracked.

In this guide we will learn how to install ErrorTracker in an Elixir project so you can start reporting errors as soon as possible. We will also cover more advanced topics such as how to report custom errors and how to add extra context to reported errors.

**This guide requires you to have set up Ecto with PostgreSQL, MySQL/MariaDB or SQLite3 beforehand.**

## Automatic installation using Igniter

The ErrorTracker includes an [igniter](https://hex.pm/packages/igniter) installer that will add the latest version of ErrorTracker to your dependencies before running the installer. Installation will use the application's default Ecto repo and Phoenix router, configure ErrorTracker and create the necessary database migrations. It will basically automate all the installation steps listed in the [manual installation](#manual-installation) section.

### If Igniter is already available

ErrorTracker may be installed and configured with a single command:

```bash
mix igniter.install error_tracker
```

### If Igniter is not yet available

If the `igniter.install` escript is not available. First, add `error_tracker` and `igniter` to your deps in `mix.exs`:

```elixir
{:error_tracker, "~> 0.8"},
{:igniter, "~> 0.5", only: [:dev]},
```

Run `mix deps.get` to fetch the dependencies, then run the install task:

```bash
mix error_tracker.install
```


## Manual Installation

The first step to add ErrorTracker to your application is to declare the package as a dependency in your `mix.exs` file:

```elixir
# mix.exs
defp deps do
  [
    {:error_tracker, "~> 0.8"}
  ]
end
```

Once ErrorTracker is declared as a dependency of your application, you can install it with the following command:

```bash
mix deps.get
```

### Configuring ErrorTracker

ErrorTracker needs a few configuration options to work. This configuration should be added to your `config/config.exs` file:

```elixir
config :error_tracker,
  repo: MyApp.Repo,
  otp_app: :my_app,
  enabled: true
```

The `:repo` option specifies the repository that will be used by ErrorTracker. You can use your regular application repository or a different one if you prefer to keep errors in a different database.

The `:otp_app` option specifies your application name. When an error occurs, ErrorTracker will use this information to understand which parts of the stack trace belong to your application and which parts belong to third-party dependencies. This allows you to filter in-app vs third-party frames when viewing errors.

The `:enabled` option (defaults to `true` if not present) allows to disable the ErrorTracker on certain environments. This is useful to avoid filling your dev database with errors, for example.

### Setting up the database

Since ErrorTracker stores errors in the database you must create a database migration to add the required tables:

```
mix ecto.gen.migration add_error_tracker
```

Open the generated migration and call the `up` and `down` functions on `ErrorTracker.Migration`:

```elixir
defmodule MyApp.Repo.Migrations.AddErrorTracker do
  use Ecto.Migration

  def up, do: ErrorTracker.Migration.up(version: 5)

  # We specify `version: 1` in `down`, to ensure we remove all migrations.
  def down, do: ErrorTracker.Migration.down(version: 1)
end
```

You can run the migration and apply the database changes with the following command:

```bash
mix ecto.migrate
```

For more information about how to handle migrations, take a look at the `ErrorTracker.Migration` module docs.

## Automatic error tracking

At this point, ErrorTracker is ready to track errors. It will automatically start when your application boots and track errors that occur in your Phoenix controllers, Phoenix LiveViews and Oban jobs. The `ErrorTracker.Integrations.Phoenix` and `ErrorTracker.Integrations.Oban` provide detailed information about how this works.

If your application uses Plug but not Phoenix, you will need to add the relevant integration in your `Plug.Builder` or `Plug.Router` module.

```elixir
defmodule MyApp.Router do
  use Plug.Router
  use ErrorTracker.Integrations.Plug

  # Your code here
end
```

This is also required if you want to track errors that happen in your Phoenix endpoint, before the Phoenix router starts handling the request. Keep in mind that this won't be needed in most cases as endpoint errors are infrequent.

```elixir
defmodule MyApp.Endpoint do
  use Phoenix.Endpoint
  use ErrorTracker.Integrations.Plug

  # Your code here
end
```

You can learn more about this in the `ErrorTracker.Integrations.Plug` module documentation.

## Error context

The default integrations include some additional context when tracking errors. You can take a look at the relevant integration modules to see what is being tracked out of the box.

In certain cases, you may want to include some additional information when tracking errors. For example it may be useful to track the user ID that was using the application when an error happened. Fortunately, ErrorTracker allows you to enrich the default context with custom information.

The `ErrorTracker.set_context/1` function stores the given context in the current process so any errors that occur in that process (for example, a Phoenix request or an Oban job) will include this given context along with the default integration context.

There are some requirements on the type of data that can be included in the context, so we recommend taking a look at `ErrorTracker.set_context/1` documentation

```elixir
ErrorTracker.set_context(%{user_id: conn.assigns.current_user.id})
```

You may also want to sanitize or filter out some information from the context before saving it. To do that you can take a look at the `ErrorTracker.Filter` behaviour.

## Manual error tracking

If you want to report custom errors that fall outside the default integration scope, you may use `ErrorTracker.report/2`. This allows you to report an exception yourself:

```elixir
try do
  # your code
catch
  e ->
    ErrorTracker.report(e, __STACKTRACE__)
end
```

You can also use `ErrorTracker.report/3` and set some custom context that will be included along with the reported error.

## Web UI

ErrorTracker also provides a dashboard built with Phoenix LiveView that can be used to see and manage the recorded errors.

This is completely optional, and you can find more information about it in the `ErrorTracker.Web` module documentation.

## Notifications

Currently ErrorTracker does not support notifications out of the box.

However, it provides some detailed Telemetry events that you may use to implement your own notifications following your custom rules and notification channels.

If you want to take a look at the events you can attach to, take a look at `ErrorTracker.Telemetry` module documentation.

## Pruning resolved errors

By default errors are kept in the database indefinitely. This is not ideal for production
environments where you may want to prune old errors that have been resolved.

The `ErrorTracker.Plugins.Pruner` module provides automatic pruning functionality with a configurable
interval and error age.

## Ignoring and Muting Errors

ErrorTracker provides two different ways to silence errors:

### Ignoring Errors

ErrorTracker tracks every error by default. In certain cases some errors may be expected or just not interesting to track.
The `ErrorTracker.Ignorer` behaviour allows you to ignore errors based on their attributes and context.

When an error is ignored, its occurrences are not tracked at all. This is useful for expected errors that you don't want to store in your database.

For example, if you had an integration with an unreliable third-party system that was frequently timing out, you could ignore those errors like so:

```elixir
defmodule MyApp.ErrorIgnores do
  @behaviour ErrorTracker.Ignorer

  @impl ErrorTracker.Ignorer
  def ignore?(%{kind: "Elixir.UnreliableThirdParty.Error", reason: ":timeout"} = _error, _context) do
    true
  end
end
```

### Muting Errors

Sometimes you may want to keep tracking error occurrences but avoid receiving notifications about them. For these cases,
ErrorTracker allows you to mute specific errors.

When an error is muted:
- New occurrences are still tracked and stored in the database
- You can still see the error and its occurrences in the web UI
- [Telemetry events](ErrorTracker.Telemetry.html) for new occurrences include the `muted: true` flag so you can ignore them as needed.

This is particularly useful for noisy errors that you want to keep tracking but don't want to receive notifications about.

You can mute and unmute errors manually through the web UI or programmatically using the `ErrorTracker.mute/1` and `ErrorTracker.unmute/1` functions.


================================================
FILE: lib/error_tracker/application.ex
================================================
defmodule ErrorTracker.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = Application.get_env(:error_tracker, :plugins, [])

    attach_handlers()

    Supervisor.start_link(children, strategy: :one_for_one, name: __MODULE__)
  end

  defp attach_handlers do
    ErrorTracker.Integrations.Oban.attach()
    ErrorTracker.Integrations.Phoenix.attach()
  end
end


================================================
FILE: lib/error_tracker/filter.ex
================================================
defmodule ErrorTracker.Filter do
  @moduledoc """
  Behaviour for sanitizing & modifying the error context before it's saved.

      defmodule MyApp.ErrorFilter do
        @behaviour ErrorTracker.Filter

        @impl true
        def sanitize(context) do
          context # Modify the context object (add or remove fields as much as you need.)
        end
      end

  Once implemented, include it in the ErrorTracker configuration:

    config :error_tracker, filter: MyApp.Filter

  With this configuration in place, the ErrorTracker will call `MyApp.Filter.sanitize/1` to get a context before
  saving error occurrence.

  > #### A note on performance {: .warning}
  >
  > Keep in mind that the `sanitize/1` will be called in the context of the ErrorTracker itself.
  > Slow code will have a significant impact in the ErrorTracker performance. Buggy code can bring
  > the ErrorTracker process down.
  """

  @doc """
  This function will be given an error context to inspect/modify before it's saved.
  """
  @callback sanitize(context :: map()) :: map()
end


================================================
FILE: lib/error_tracker/ignorer.ex
================================================
defmodule ErrorTracker.Ignorer do
  @moduledoc """
  Behaviour for ignoring errors.

  > #### Ignoring vs muting errors {: .info}
  >
  > Ignoring an error keeps it from being tracked by the ErrorTracker. While this may be useful in
  > certain cases, in other cases you may prefer to track the error but don't send telemetry events.
  > Take a look at the `ErrorTracker.mute/1` function to see how to mute errors.

  The ErrorTracker tracks every error that happens in your application. In certain cases you may
  want to ignore some errors and don't track them. To do so you can implement this behaviour.

      defmodule MyApp.ErrorIgnorer do
        @behaviour ErrorTracker.Ignorer

        @impl true
        def ignore?(error = %ErrorTracker.Error{}, context) do
          # return true if the error should be ignored
        end
      end

  Once implemented, include it in the ErrorTracker configuration:

      config :error_tracker, ignorer: MyApp.ErrorIgnorer

  With this configuration in place, the ErrorTracker will call `MyApp.ErrorIgnorer.ignore?/2` before
  tracking errors. If the function returns `true` the error will be ignored and won't be tracked.

  > #### A note on performance {: .warning}
  >
  > Keep in mind that the `ignore?/2` will be called in the context of the ErrorTracker itself.
  > Slow code will have a significant impact in the ErrorTracker performance. Buggy code can bring
  > the ErrorTracker process down.
  """

  @doc """
  Decide wether the given error should be ignored or not.

  This function receives both the current Error and context and should return a boolean indicating
  if it should be ignored or not. If the function returns true the error will be ignored, otherwise
  it will be tracked.
  """
  @callback ignore?(error :: ErrorTracker.Error.t(), context :: map()) :: boolean
end


================================================
FILE: lib/error_tracker/integrations/oban.ex
================================================
defmodule ErrorTracker.Integrations.Oban do
  @moduledoc """
  Integration with Oban.

  ## How to use it

  It is a plug and play integration: as long as you have Oban installed the
  ErrorTracker will receive and store the errors as they are reported.

  ### How it works

  It works using Oban's Telemetry events, so you don't need to modify anything
  on your application.

  > #### A note on errors grouping {: .warning}
  >
  > All errors reported using `:error` or `{:error, any()}` as the output of
  > your `perform/2` worker function are going to be grouped together (one group
  > of those of errors per worker).
  >
  > The reason of that behaviour is that those errors do not generate an exception,
  > so no stack trace is detected and they are stored as happening in the same
  > place.
  >
  > If you want errors of your workers to be grouped as you may expect on other
  > integrations, you should raise exceptions to report errors instead of gracefully
  > returning an error value.

  ### Default context

  By default we store some context for you on errors generated in an Oban
  process:

  * `job.id`: the unique ID of the job.

  * `job.worker`: the name of the worker module.

  * `job.queue`: the name of the queue in which the job was inserted.

  * `job.args`: the arguments of the job being executed.

  * `job.priority`: the priority of the job.

  * `job.attempt`: the number of attempts performed for the job.
  """

  # https://hexdocs.pm/oban/Oban.Telemetry.html
  @events [
    [:oban, :job, :start],
    [:oban, :job, :exception]
  ]

  @doc false
  def attach do
    if Application.spec(:oban) do
      :telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config)
    end
  end

  @doc false
  def handle_event([:oban, :job, :start], _measurements, metadata, :no_config) do
    %{job: job} = metadata

    ErrorTracker.set_context(%{
      "job.args" => job.args,
      "job.attempt" => job.attempt,
      "job.id" => job.id,
      "job.priority" => job.priority,
      "job.queue" => job.queue,
      "job.worker" => job.worker
    })
  end

  def handle_event([:oban, :job, :exception], _measurements, metadata, :no_config) do
    %{reason: exception, stacktrace: stacktrace, job: job} = metadata
    state = Map.get(metadata, :state, :failure)

    stacktrace =
      if stacktrace == [],
        do: [{String.to_existing_atom("Elixir." <> job.worker), :perform, 2, []}],
        else: stacktrace

    ErrorTracker.report(exception, stacktrace, %{state: state})
  end
end


================================================
FILE: lib/error_tracker/integrations/phoenix.ex
================================================
defmodule ErrorTracker.Integrations.Phoenix do
  @moduledoc """
  Integration with Phoenix applications.

  ## How to use it

  It is a plug and play integration: as long as you have Phoenix installed the
  ErrorTracker will receive and store the errors as they are reported.

  It also collects the exceptions that raise on your LiveView modules.

  ### How it works

  It works using Phoenix's Telemetry events, so you don't need to modify
  anything on your application.

  ### Errors on the Endpoint

  This integration only catches errors that raise after the requests hits your
  Router. That means that an exception on a plug defined on your Endpoint will
  not be reported.

  If you want to also catch those errors, we recommend you to set up the
  `ErrorTracker.Integrations.Plug` integration too.

  ### Default context

  For errors that are reported when executing regular HTTP requests (the ones
  that go to Controllers), the context added by default is the same that you
  can find on the `ErrorTracker.Integrations.Plug` integration.

  As for exceptions generated in LiveView processes, we collect some special
  information on the context:

  * `live_view.view`: the LiveView module itself,

  * `live_view.uri`: last URI that loaded the LiveView (available when the
  `handle_params` function is invoked).

  * `live_view.params`: the params received by the LiveView (available when the
  `handle_params` function is invoked).

  * `live_view.event`: last event received by the LiveView (available when the
  `handle_event` function is invoked).

  * `live_view.event_params`: last event params received by the LiveView
  (available when the `handle_event` function is invoked).
  """

  alias ErrorTracker.Integrations.Plug, as: PlugIntegration

  @events [
    # https://hexdocs.pm/phoenix/Phoenix.Logger.html#module-instrumentation
    [:phoenix, :router_dispatch, :start],
    [:phoenix, :router_dispatch, :exception],
    # https://hexdocs.pm/phoenix_live_view/telemetry.html
    [:phoenix, :live_view, :mount, :start],
    [:phoenix, :live_view, :mount, :exception],
    [:phoenix, :live_view, :handle_params, :start],
    [:phoenix, :live_view, :handle_params, :exception],
    [:phoenix, :live_view, :handle_event, :exception],
    [:phoenix, :live_view, :render, :exception],
    [:phoenix, :live_component, :update, :exception],
    [:phoenix, :live_component, :handle_event, :exception]
  ]

  @doc false
  def attach do
    if Application.spec(:phoenix) do
      :telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config)
    end
  end

  @doc false
  def handle_event([:phoenix, :router_dispatch, :start], _measurements, metadata, :no_config) do
    PlugIntegration.set_context(metadata.conn)
  end

  def handle_event([:phoenix, :router_dispatch, :exception], _measurements, metadata, :no_config) do
    {reason, kind, stack} =
      case metadata do
        %{reason: %Plug.Conn.WrapperError{reason: reason, kind: kind, stack: stack}} ->
          {reason, kind, stack}

        %{kind: kind, reason: reason, stacktrace: stack} ->
          {reason, kind, stack}
      end

    PlugIntegration.report_error(metadata.conn, {kind, reason}, stack)
  end

  def handle_event([:phoenix, :live_view, :mount, :start], _, metadata, :no_config) do
    ErrorTracker.set_context(%{
      "live_view.view" => metadata.socket.view
    })
  end

  def handle_event([:phoenix, :live_view, :handle_params, :start], _, metadata, :no_config) do
    ErrorTracker.set_context(%{
      "live_view.uri" => metadata.uri,
      "live_view.params" => metadata.params
    })
  end

  def handle_event([:phoenix, :live_view, :handle_event, :exception], _, metadata, :no_config) do
    ErrorTracker.report({metadata.kind, metadata.reason}, metadata.stacktrace, %{
      "live_view.event" => metadata.event,
      "live_view.event_params" => metadata.params
    })
  end

  def handle_event([:phoenix, :live_view, _action, :exception], _, metadata, :no_config) do
    ErrorTracker.report({metadata.kind, metadata.reason}, metadata.stacktrace)
  end

  def handle_event([:phoenix, :live_component, :update, :exception], _, metadata, :no_config) do
    ErrorTracker.report({metadata.kind, metadata.reason}, metadata.stacktrace, %{
      "live_view.component" => metadata.component
    })
  end

  def handle_event([:phoenix, :live_component, :handle_event, :exception], _, metadata, :no_config) do
    ErrorTracker.report({metadata.kind, metadata.reason}, metadata.stacktrace, %{
      "live_view.component" => metadata.component,
      "live_view.event" => metadata.event,
      "live_view.event_params" => metadata.params
    })
  end
end


================================================
FILE: lib/error_tracker/integrations/plug.ex
================================================
defmodule ErrorTracker.Integrations.Plug do
  @moduledoc """
  Integration with Plug applications.

  ## How to use it

  ### Plug applications

  The way to use this integration is by adding it to either your `Plug.Builder`
  or `Plug.Router`:

  ```elixir
  defmodule MyApp.Router do
    use Plug.Router
    use ErrorTracker.Integrations.Plug

    ...
  end
  ```

  ### Phoenix applications

  There is a particular use case which can be useful when running a Phoenix
  web application.

  If you want to record exceptions that may occur in your application's endpoint
  before reaching your router (for example, in any plug like the ones decoding
  cookies of body contents) you may want to add this integration too:

  ```elixir
  defmodule MyApp.Endpoint do
    use Phoenix.Endpoint
    use ErrorTracker.Integrations.Plug

    ...
  end
  ```

  ### Default context

  By default we store some context for you on errors generated during a Plug
  request:

  * `request.host`: the `conn.host` value.

  * `request.ip`: the IP address that initiated the request. It includes parsing
  proxy headers

  * `request.method`: the HTTP method of the request.

  * `request.path`: the path of the request.

  * `request.query`: the query string of the request.

  * `request.params`: parsed params of the request (only available if they have
  been fetched and parsed as part of the Plug pipeline).

  * `request.headers`: headers received on the request. All headers are included
  by default except for the `Cookie` ones, as they may include large and
  sensitive content like sessions.

  """

  defmacro __using__(_opts) do
    quote do
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_) do
    quote do
      defoverridable call: 2

      def call(conn, opts) do
        unquote(__MODULE__).set_context(conn)
        super(conn, opts)
      rescue
        e in Plug.Conn.WrapperError ->
          unquote(__MODULE__).report_error(e.conn, e.reason, e.stack)

          Plug.Conn.WrapperError.reraise(e)

        e ->
          stack = __STACKTRACE__
          unquote(__MODULE__).report_error(conn, e, stack)

          :erlang.raise(:error, e, stack)
      catch
        kind, reason ->
          stack = __STACKTRACE__
          unquote(__MODULE__).report_error(conn, {kind, reason}, stack)

          :erlang.raise(kind, reason, stack)
      end
    end
  end

  @doc false
  def report_error(conn, reason, stack) do
    if !Process.get(:error_tracker_router_exception_reported) do
      try do
        ErrorTracker.report(reason, stack, build_context(conn))
      after
        Process.put(:error_tracker_router_exception_reported, true)
      end
    end
  end

  @doc false
  def set_context(%Plug.Conn{} = conn) do
    conn |> build_context() |> ErrorTracker.set_context()
  end

  @sensitive_headers ~w[authorization cookie set-cookie]

  defp build_context(%Plug.Conn{} = conn) do
    %{
      "request.host" => conn.host,
      "request.path" => conn.request_path,
      "request.query" => conn.query_string,
      "request.method" => conn.method,
      "request.ip" => remote_ip(conn),
      "request.headers" =>
        Map.new(conn.req_headers, fn {header, value} ->
          if header in @sensitive_headers, do: {header, "[REDACTED]"}, else: {header, value}
        end),
      # Depending on the error source, the request params may have not been fetched yet
      "request.params" => if(!is_struct(conn.params, Plug.Conn.Unfetched), do: conn.params)
    }
  end

  defp remote_ip(%Plug.Conn{} = conn) do
    remote_ip =
      case Plug.Conn.get_req_header(conn, "x-forwarded-for") do
        [x_forwarded_for | _] ->
          x_forwarded_for |> String.split(",", parts: 2) |> List.first()

        [] ->
          case :inet.ntoa(conn.remote_ip) do
            {:error, _} -> ""
            address -> to_string(address)
          end
      end

    String.trim(remote_ip)
  end
end


================================================
FILE: lib/error_tracker/migration/mysql/v03.ex
================================================
defmodule ErrorTracker.Migration.MySQL.V03 do
  @moduledoc false

  use Ecto.Migration

  def up(_opts) do
    create table(:error_tracker_meta, primary_key: [name: :key, type: :string]) do
      add :value, :string, null: false
    end

    create table(:error_tracker_errors, primary_key: [name: :id, type: :bigserial]) do
      add :kind, :string, null: false
      add :reason, :text, null: false
      add :source_line, :text, null: false
      add :source_function, :text, null: false
      add :status, :string, null: false
      add :fingerprint, :string, null: false
      add :last_occurrence_at, :utc_datetime_usec, null: false

      timestamps(type: :utc_datetime_usec)
    end

    create unique_index(:error_tracker_errors, [:fingerprint])

    create table(:error_tracker_occurrences, primary_key: [name: :id, type: :bigserial]) do
      add :context, :map, null: false
      add :reason, :text, null: false
      add :stacktrace, :map, null: false

      add :error_id,
          references(:error_tracker_errors,
            on_delete: :delete_all,
            column: :id,
            type: :bigserial
          ),
          null: false

      timestamps(type: :utc_datetime_usec, updated_at: false)
    end

    create index(:error_tracker_occurrences, [:error_id])

    create index(:error_tracker_errors, [:last_occurrence_at])
  end

  def down(_opts) do
    drop table(:error_tracker_occurrences)
    drop table(:error_tracker_errors)
    drop table(:error_tracker_meta)
  end
end


================================================
FILE: lib/error_tracker/migration/mysql/v04.ex
================================================
defmodule ErrorTracker.Migration.MySQL.V04 do
  @moduledoc false

  use Ecto.Migration

  def up(_opts) do
    alter table(:error_tracker_occurrences) do
      add :breadcrumbs, :json, null: true
    end
  end

  def down(_opts) do
    alter table(:error_tracker_occurrences) do
      remove :breadcrumbs
    end
  end
end


================================================
FILE: lib/error_tracker/migration/mysql/v05.ex
================================================
defmodule ErrorTracker.Migration.MySQL.V05 do
  @moduledoc false

  use Ecto.Migration

  def up(_opts) do
    alter table(:error_tracker_errors) do
      add :muted, :boolean, default: false, null: false
    end
  end

  def down(_opts) do
    alter table(:error_tracker_errors) do
      remove :muted
    end
  end
end


================================================
FILE: lib/error_tracker/migration/mysql.ex
================================================
defmodule ErrorTracker.Migration.MySQL do
  @moduledoc false

  @behaviour ErrorTracker.Migration

  use Ecto.Migration

  alias ErrorTracker.Migration.SQLMigrator

  @initial_version 3
  @current_version 5

  @impl ErrorTracker.Migration
  def up(opts) do
    opts = with_defaults(opts, @current_version)
    SQLMigrator.migrate_up(__MODULE__, opts, @initial_version)
  end

  @impl ErrorTracker.Migration
  def down(opts) do
    opts = with_defaults(opts, @initial_version)
    SQLMigrator.migrate_down(__MODULE__, opts, @initial_version)
  end

  @impl ErrorTracker.Migration
  def current_version(opts) do
    opts = with_defaults(opts, @initial_version)
    SQLMigrator.current_version(opts)
  end

  defp with_defaults(opts, version) do
    Enum.into(opts, %{version: version})
  end
end


================================================
FILE: lib/error_tracker/migration/postgres/v01.ex
================================================
defmodule ErrorTracker.Migration.Postgres.V01 do
  @moduledoc false

  use Ecto.Migration

  import Ecto.Query

  def up(%{create_schema: create_schema, prefix: prefix} = opts) do
    # Prior to V02 the migration version was stored in table comments.
    # As of now the migration version is stored in a new table (created in V02).
    #
    # However, systems migrating to V02 may think they need to run V01 too, so
    # we need to check for the legacy version storage to avoid running this
    # migration twice.
    if current_version_legacy(opts) == 0 do
      if create_schema, do: execute("CREATE SCHEMA IF NOT EXISTS #{prefix}")

      create table(:error_tracker_meta,
               primary_key: [name: :key, type: :string],
               prefix: prefix
             ) do
        add :value, :string, null: false
      end

      create table(:error_tracker_errors,
               primary_key: [name: :id, type: :bigserial],
               prefix: prefix
             ) do
        add :kind, :string, null: false
        add :reason, :text, null: false
        add :source_line, :text, null: false
        add :source_function, :text, null: false
        add :status, :string, null: false
        add :fingerprint, :string, null: false
        add :last_occurrence_at, :utc_datetime_usec, null: false

        timestamps(type: :utc_datetime_usec)
      end

      create unique_index(:error_tracker_errors, [:fingerprint], prefix: prefix)

      create table(:error_tracker_occurrences,
               primary_key: [name: :id, type: :bigserial],
               prefix: prefix
             ) do
        add :context, :map, null: false
        add :reason, :text, null: false
        add :stacktrace, :map, null: false

        add :error_id,
            references(:error_tracker_errors,
              on_delete: :delete_all,
              column: :id,
              type: :bigserial
            ),
            null: false

        timestamps(type: :utc_datetime_usec, updated_at: false)
      end

      create index(:error_tracker_occurrences, [:error_id], prefix: prefix)
    else
      :noop
    end
  end

  def down(%{prefix: prefix}) do
    drop table(:error_tracker_occurrences, prefix: prefix)
    drop table(:error_tracker_errors, prefix: prefix)
    drop_if_exists table(:error_tracker_meta, prefix: prefix)
  end

  def current_version_legacy(opts) do
    query =
      from pg_class in "pg_class",
        left_join: pg_description in "pg_description",
        on: pg_description.objoid == pg_class.oid,
        left_join: pg_namespace in "pg_namespace",
        on: pg_namespace.oid == pg_class.relnamespace,
        where: pg_class.relname == "error_tracker_errors",
        where: pg_namespace.nspname == ^opts.escaped_prefix,
        select: pg_description.description

    case repo().one(query, log: false) do
      version when is_binary(version) -> String.to_integer(version)
      _other -> 0
    end
  end
end


================================================
FILE: lib/error_tracker/migration/postgres/v02.ex
================================================
defmodule ErrorTracker.Migration.Postgres.V02 do
  @moduledoc false

  use Ecto.Migration

  def up(%{prefix: prefix}) do
    # For systems which executed versions without this migration they may not
    # have the error_tracker_meta table, so we need to create it conditionally
    # to avoid errors.
    create_if_not_exists table(:error_tracker_meta,
                           primary_key: [name: :key, type: :string],
                           prefix: prefix
                         ) do
      add :value, :string, null: false
    end

    execute "COMMENT ON TABLE #{inspect(prefix)}.error_tracker_errors IS ''"
  end

  def down(%{prefix: prefix}) do
    # We do not delete the `error_tracker_meta` table because it's creation and
    # deletion are controlled by V01 migration.
    execute "COMMENT ON TABLE #{inspect(prefix)}.error_tracker_errors IS '1'"
  end
end


================================================
FILE: lib/error_tracker/migration/postgres/v03.ex
================================================
defmodule ErrorTracker.Migration.Postgres.V03 do
  @moduledoc false

  use Ecto.Migration

  def up(%{prefix: prefix}) do
    create_if_not_exists index(:error_tracker_errors, [:last_occurrence_at], prefix: prefix)
  end

  def down(%{prefix: prefix}) do
    drop_if_exists index(:error_tracker_errors, [:last_occurrence_at], prefix: prefix)
  end
end


================================================
FILE: lib/error_tracker/migration/postgres/v04.ex
================================================
defmodule ErrorTracker.Migration.Postgres.V04 do
  @moduledoc false

  use Ecto.Migration

  def up(%{prefix: prefix}) do
    alter table(:error_tracker_occurrences, prefix: prefix) do
      add :breadcrumbs, {:array, :string}, default: [], null: false
    end
  end

  def down(%{prefix: prefix}) do
    alter table(:error_tracker_occurrences, prefix: prefix) do
      remove :breadcrumbs
    end
  end
end


================================================
FILE: lib/error_tracker/migration/postgres/v05.ex
================================================
defmodule ErrorTracker.Migration.Postgres.V05 do
  @moduledoc false

  use Ecto.Migration

  def up(%{prefix: prefix}) do
    alter table(:error_tracker_errors, prefix: prefix) do
      add :muted, :boolean, default: false, null: false
    end
  end

  def down(%{prefix: prefix}) do
    alter table(:error_tracker_errors, prefix: prefix) do
      remove :muted
    end
  end
end


================================================
FILE: lib/error_tracker/migration/postgres.ex
================================================
defmodule ErrorTracker.Migration.Postgres do
  @moduledoc false

  @behaviour ErrorTracker.Migration

  use Ecto.Migration

  alias ErrorTracker.Migration.SQLMigrator

  @initial_version 1
  @current_version 5
  @default_prefix "public"

  @impl ErrorTracker.Migration
  def up(opts) do
    opts = with_defaults(opts, @current_version)
    SQLMigrator.migrate_up(__MODULE__, opts, @initial_version)
  end

  @impl ErrorTracker.Migration
  def down(opts) do
    opts = with_defaults(opts, @initial_version)
    SQLMigrator.migrate_down(__MODULE__, opts, @initial_version)
  end

  @impl ErrorTracker.Migration
  def current_version(opts) do
    opts = with_defaults(opts, @initial_version)
    SQLMigrator.current_version(opts)
  end

  defp with_defaults(opts, version) do
    configured_prefix = Application.get_env(:error_tracker, :prefix, "public")
    opts = Enum.into(opts, %{prefix: configured_prefix, version: version})

    opts
    |> Map.put_new(:create_schema, opts.prefix != @default_prefix)
    |> Map.put_new(:escaped_prefix, String.replace(opts.prefix, "'", "\\'"))
  end
end


================================================
FILE: lib/error_tracker/migration/sql_migrator.ex
================================================
defmodule ErrorTracker.Migration.SQLMigrator do
  @moduledoc false

  use Ecto.Migration

  import Ecto.Query

  alias Ecto.Adapters.SQL

  def migrate_up(migrator, opts, initial_version) do
    initial = current_version(opts)

    cond do
      initial == 0 ->
        change(migrator, initial_version..opts.version, :up, opts)

      initial < opts.version ->
        change(migrator, (initial + 1)..opts.version, :up, opts)

      true ->
        :ok
    end
  end

  def migrate_down(migrator, opts, initial_version) do
    initial = max(current_version(opts), initial_version)

    if initial >= opts.version do
      change(migrator, initial..opts.version//-1, :down, opts)
    end
  end

  def current_version(opts) do
    repo = Map.get_lazy(opts, :repo, fn -> repo() end)

    query =
      from meta in "error_tracker_meta",
        where: meta.key == "migration_version",
        select: meta.value

    with true <- meta_table_exists?(repo, opts),
         version when is_binary(version) <- repo.one(query, log: false, prefix: opts[:prefix]) do
      String.to_integer(version)
    else
      _other -> 0
    end
  end

  defp change(migrator, versions_range, direction, opts) do
    for version <- versions_range do
      padded_version = String.pad_leading(to_string(version), 2, "0")

      migration_module = Module.concat(migrator, "V#{padded_version}")
      apply(migration_module, direction, [opts])
    end

    case direction do
      :up -> record_version(opts, Enum.max(versions_range))
      :down -> record_version(opts, Enum.min(versions_range) - 1)
    end
  end

  defp record_version(_opts, 0), do: :ok

  defp record_version(opts, version) do
    timestamp = DateTime.to_unix(DateTime.utc_now())

    ErrorTracker.Repo.with_adapter(fn
      :postgres ->
        prefix = opts[:prefix]

        execute """
        INSERT INTO #{prefix}.error_tracker_meta (key, value)
        VALUES ('migration_version', '#{version}'), ('migration_timestamp', '#{timestamp}')
        ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
        """

      :mysql ->
        execute """
        INSERT INTO error_tracker_meta (`key`, value)
        VALUES ('migration_version', '#{version}'), ('migration_timestamp', '#{timestamp}')
        ON DUPLICATE KEY UPDATE value = VALUES(value)
        """

      _other ->
        execute """
        INSERT INTO error_tracker_meta (key, value)
        VALUES ('migration_version', '#{version}'), ('migration_timestamp', '#{timestamp}')
        ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
        """
    end)
  end

  defp meta_table_exists?(repo, opts) do
    ErrorTracker.Repo.with_adapter(fn
      :postgres ->
        repo
        |> SQL.query!(
          "SELECT TRUE FROM information_schema.tables WHERE table_name = 'error_tracker_meta' AND table_schema = $1",
          [opts.prefix],
          log: false
        )
        |> Map.get(:rows)
        |> Enum.any?()

      _other ->
        SQL.table_exists?(repo, "error_tracker_meta", log: false)
    end)
  end
end


================================================
FILE: lib/error_tracker/migration/sqlite/v02.ex
================================================
defmodule ErrorTracker.Migration.SQLite.V02 do
  @moduledoc false

  use Ecto.Migration

  def up(_opts) do
    create table(:error_tracker_meta, primary_key: [name: :key, type: :string]) do
      add :value, :string, null: false
    end

    create table(:error_tracker_errors, primary_key: [name: :id, type: :bigserial]) do
      add :kind, :string, null: false
      add :reason, :text, null: false
      add :source_line, :text, null: false
      add :source_function, :text, null: false
      add :status, :string, null: false
      add :fingerprint, :string, null: false
      add :last_occurrence_at, :utc_datetime_usec, null: false

      timestamps(type: :utc_datetime_usec)
    end

    create unique_index(:error_tracker_errors, [:fingerprint])

    create table(:error_tracker_occurrences, primary_key: [name: :id, type: :bigserial]) do
      add :context, :map, null: false
      add :reason, :text, null: false
      add :stacktrace, :map, null: false

      add :error_id,
          references(:error_tracker_errors,
            on_delete: :delete_all,
            column: :id,
            type: :bigserial
          ),
          null: false

      timestamps(type: :utc_datetime_usec, updated_at: false)
    end

    create index(:error_tracker_occurrences, [:error_id])
  end

  def down(_opts) do
    drop table(:error_tracker_occurrences)
    drop table(:error_tracker_errors)
    drop table(:error_tracker_meta)
  end
end


================================================
FILE: lib/error_tracker/migration/sqlite/v03.ex
================================================
defmodule ErrorTracker.Migration.SQLite.V03 do
  @moduledoc false

  use Ecto.Migration

  def up(_opts) do
    create_if_not_exists index(:error_tracker_errors, [:last_occurrence_at])
  end

  def down(_opts) do
    drop_if_exists index(:error_tracker_errors, [:last_occurrence_at])
  end
end


================================================
FILE: lib/error_tracker/migration/sqlite/v04.ex
================================================
defmodule ErrorTracker.Migration.SQLite.V04 do
  @moduledoc false

  use Ecto.Migration

  def up(_opts) do
    alter table(:error_tracker_occurrences) do
      add :breadcrumbs, {:array, :string}, default: [], null: false
    end
  end

  def down(_opts) do
    alter table(:error_tracker_occurrences) do
      remove :breadcrumbs
    end
  end
end


================================================
FILE: lib/error_tracker/migration/sqlite/v05.ex
================================================
defmodule ErrorTracker.Migration.SQLite.V05 do
  @moduledoc false

  use Ecto.Migration

  def up(_opts) do
    alter table(:error_tracker_errors) do
      add :muted, :boolean, default: false, null: false
    end
  end

  def down(_opts) do
    alter table(:error_tracker_errors) do
      remove :muted
    end
  end
end


================================================
FILE: lib/error_tracker/migration/sqlite.ex
================================================
defmodule ErrorTracker.Migration.SQLite do
  @moduledoc false

  @behaviour ErrorTracker.Migration

  use Ecto.Migration

  alias ErrorTracker.Migration.SQLMigrator

  @initial_version 2
  @current_version 5

  @impl ErrorTracker.Migration
  def up(opts) do
    opts = with_defaults(opts, @current_version)
    SQLMigrator.migrate_up(__MODULE__, opts, @initial_version)
  end

  @impl ErrorTracker.Migration
  def down(opts) do
    opts = with_defaults(opts, @initial_version)
    SQLMigrator.migrate_down(__MODULE__, opts, @initial_version)
  end

  @impl ErrorTracker.Migration
  def current_version(opts) do
    opts = with_defaults(opts, @initial_version)
    SQLMigrator.current_version(opts)
  end

  defp with_defaults(opts, version) do
    Enum.into(opts, %{version: version})
  end
end


================================================
FILE: lib/error_tracker/migration.ex
================================================
defmodule ErrorTracker.Migration do
  @moduledoc """
  Create and modify the database tables for ErrorTracker.

  ## Usage

  To use ErrorTracker migrations in your application you will need to generate
  a regular `Ecto.Migration` that performs the relevant calls to `ErrorTracker.Migration`.

  ```bash
  mix ecto.gen.migration add_error_tracker
  ```

  Open the generated migration file and call the `up` and `down` functions on
  `ErrorTracker.Migration`.

  ```elixir
  defmodule MyApp.Repo.Migrations.AddErrorTracker do
    use Ecto.Migration

    def up, do: ErrorTracker.Migration.up()
    def down, do: ErrorTracker.Migration.down()
  end
  ```

  This will run every ErrorTracker migration for your database. You can now run the migration
  and perform the database changes:

  ```bash
  mix ecto.migrate
  ```

  As new versions of ErrorTracker are released you may need to run additional migrations.
  To do this you can follow the previous process and create a new migration:

  ```bash
  mix ecto.gen.migration update_error_tracker_to_vN
  ```

  Open the generated migration file and call the `up` and `down` functions on the
  `ErrorTracker.Migration` passing the desired `version`.

  ```elixir
  defmodule MyApp.Repo.Migrations.UpdateErrorTrackerToVN do
    use Ecto.Migration

    def up, do: ErrorTracker.Migration.up(version: N)
    def down, do: ErrorTracker.Migration.down(version: N)
  end
  ```

  Then run the migrations to perform the database changes:

  ```bash
  mix ecto.migrate
  ```

  ## Custom prefix - PostgreSQL only

  ErrorTracker supports namespacing its own tables using PostgreSQL schemas, also known
  as "prefixes" in Ecto. With prefixes your error tables can reside outside of your primary
  schema (which is usually named "public").

  To use a prefix you need to specify it in your configuration:

  ```elixir
  config :error_tracker, :prefix, "custom_prefix"
  ```

  Migrations will automatically create the database schema for you. If the schema does already exist
  the migration may fail when trying to recreate it. In such cases you can instruct ErrorTracker
  not to create the schema again:

  ```elixir
  defmodule MyApp.Repo.Migrations.AddErrorTracker do
    use Ecto.Migration

    def up, do: ErrorTracker.Migration.up(create_schema: false)
    def down, do: ErrorTracker.Migration.down()
  end
  ```

  You can also override the configured prefix in the migration:

  ```elixir
  defmodule MyApp.Repo.Migrations.AddErrorTracker do
    use Ecto.Migration

    def up, do: ErrorTracker.Migration.up(prefix: "custom_prefix")
    def down, do: ErrorTracker.Migration.down(prefix: "custom_prefix")
  end
  ```
  """

  @callback up(Keyword.t()) :: :ok
  @callback down(Keyword.t()) :: :ok
  @callback current_version(Keyword.t()) :: non_neg_integer()

  @spec up(Keyword.t()) :: :ok
  def up(opts \\ []) when is_list(opts) do
    migrator().up(opts)
  end

  @spec down(Keyword.t()) :: :ok
  def down(opts \\ []) when is_list(opts) do
    migrator().down(opts)
  end

  @spec migrated_version(Keyword.t()) :: non_neg_integer()
  def migrated_version(opts \\ []) when is_list(opts) do
    migrator().migrated_version(opts)
  end

  defp migrator do
    ErrorTracker.Repo.with_adapter(fn
      :postgres -> ErrorTracker.Migration.Postgres
      :mysql -> ErrorTracker.Migration.MySQL
      :sqlite -> ErrorTracker.Migration.SQLite
      adapter -> raise "ErrorTracker does not support #{adapter}"
    end)
  end
end


================================================
FILE: lib/error_tracker/plugins/pruner.ex
================================================
defmodule ErrorTracker.Plugins.Pruner do
  @moduledoc """
  Periodically delete resolved errors based on their age.

  Pruning allows you to keep your database size under control by removing old errors that are not
  needed anymore.

  ## Using the pruner

  To enable the pruner you must register the plugin in the ErrorTracker configuration. This will use
  the default options, which is to prune errors resolved after 24 hours.

      config :error_tracker,
        plugins: [ErrorTracker.Plugins.Pruner]

  You can override the default options by passing them as an argument when registering the plugin.

      config :error_tracker,
        plugins: [{ErrorTracker.Plugins.Pruner, max_age: :timer.minutes(30)}]

  ## Options

  - `:limit`  - the maximum number of errors to prune on each execution. Occurrences are removed
    along the errors. The default is 200 to prevent timeouts and unnecesary database load.

  - `:max_age` - the number of milliseconds after a resolved error may be pruned. The default is 24
    hours.

  - `:interval` - the interval in milliseconds between pruning runs. The default is 30 minutes.

  You may find the `:timer` module functions useful to pass readable values to the `:max_age` and
  `:interval` options.

  ## Manual pruning

  In certain cases you may prefer to run the pruner manually. This can be done by calling the
  `prune_errors/2` function from your application code. This function supports the `:limit` and
  `:max_age` options as described above.

  For example, you may call this function from an Oban worker so you can leverage Oban's cron
  capabilities and have a more granular control over when pruning is run.

      defmodule MyApp.ErrorPruner do
        use Oban.Worker

        def perform(%Job{}) do
          ErrorTracker.Plugins.Pruner.prune_errors(limit: 10_000, max_age: :timer.minutes(60))
        end
      end
  """
  use GenServer

  import Ecto.Query

  alias ErrorTracker.Error
  alias ErrorTracker.Occurrence
  alias ErrorTracker.Repo

  @doc """
  Prunes resolved errors.

  You do not need to use this function if you activate the Pruner plugin. This function is exposed
  only for advanced use cases and Oban integration.

  ## Options

  - `:limit`  - the maximum number of errors to prune on each execution. Occurrences are removed
    along the errors. The default is 200 to prevent timeouts and unnecesary database load.

  - `:max_age` - the number of milliseconds after a resolved error may be pruned. The default is 24
    hours. You may find the `:timer` module functions useful to pass readable values to this option.
  """
  @spec prune_errors(keyword()) :: {:ok, list(Error.t())}
  def prune_errors(opts \\ []) do
    limit = opts[:limit] || raise ":limit option is required"
    max_age = opts[:max_age] || raise ":max_age option is required"
    time = DateTime.add(DateTime.utc_now(), -max_age, :millisecond)

    errors =
      Repo.all(
        from error in Error,
          select: [:id, :kind, :source_line, :source_function],
          where: error.status == :resolved,
          where: error.last_occurrence_at < ^time,
          limit: ^limit
      )

    if Enum.any?(errors) do
      _pruned_occurrences_count =
        errors
        |> Ecto.assoc(:occurrences)
        |> prune_occurrences()
        |> Enum.sum()

      Repo.delete_all(from error in Error, where: error.id in ^Enum.map(errors, & &1.id))
    end

    {:ok, errors}
  end

  defp prune_occurrences(occurrences_query) do
    Stream.unfold(occurrences_query, fn occurrences_query ->
      occurrences_ids =
        Repo.all(from occurrence in occurrences_query, select: occurrence.id, limit: 1000)

      case Repo.delete_all(from o in Occurrence, where: o.id in ^occurrences_ids) do
        {0, _} -> nil
        {deleted, _} -> {deleted, occurrences_query}
      end
    end)
  end

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @impl GenServer
  @doc false
  def init(state \\ []) do
    state = %{
      limit: state[:limit] || 200,
      max_age: state[:max_age] || :timer.hours(24),
      interval: state[:interval] || :timer.minutes(30)
    }

    {:ok, schedule_prune(state)}
  end

  @impl GenServer
  @doc false
  def handle_info(:prune, state) do
    {:ok, _pruned} = prune_errors(state)

    {:noreply, schedule_prune(state)}
  end

  defp schedule_prune(%{interval: interval} = state) do
    Process.send_after(self(), :prune, interval)

    state
  end
end


================================================
FILE: lib/error_tracker/repo.ex
================================================
defmodule ErrorTracker.Repo do
  @moduledoc false

  def insert!(struct_or_changeset, opts \\ []) do
    dispatch(:insert!, [struct_or_changeset], opts)
  end

  def update(changeset, opts \\ []) do
    dispatch(:update, [changeset], opts)
  end

  def get(queryable, id, opts \\ []) do
    dispatch(:get, [queryable, id], opts)
  end

  def get!(queryable, id, opts \\ []) do
    dispatch(:get!, [queryable, id], opts)
  end

  def one(queryable, opts \\ []) do
    dispatch(:one, [queryable], opts)
  end

  def all(queryable, opts \\ []) do
    dispatch(:all, [queryable], opts)
  end

  def delete_all(queryable, opts \\ []) do
    dispatch(:delete_all, [queryable], opts)
  end

  def aggregate(queryable, aggregate, opts \\ []) do
    dispatch(:aggregate, [queryable, aggregate], opts)
  end

  def transaction(fun_or_multi, opts \\ []) do
    dispatch(:transaction, [fun_or_multi], opts)
  end

  def with_adapter(fun) do
    adapter =
      case repo().__adapter__() do
        Ecto.Adapters.Postgres -> :postgres
        Ecto.Adapters.MyXQL -> :mysql
        Ecto.Adapters.SQLite3 -> :sqlite
      end

    fun.(adapter)
  end

  defp dispatch(action, args, opts) do
    repo = repo()

    defaults =
      with_adapter(fn
        :postgres -> [prefix: Application.get_env(:error_tracker, :prefix, "public")]
        _ -> []
      end)

    opts_w_defaults = Keyword.merge(defaults, opts)

    apply(repo, action, args ++ [opts_w_defaults])
  end

  defp repo do
    Application.fetch_env!(:error_tracker, :repo)
  end
end


================================================
FILE: lib/error_tracker/schemas/error.ex
================================================
defmodule ErrorTracker.Error do
  @moduledoc """
  Schema to store an error or exception recorded by ErrorTracker.

  It stores a kind, reason and source code location to generate a unique
  fingerprint that can be used to avoid duplicates.

  The fingerprint currently does not include the reason itself because it can
  contain specific details that can change on the same error depending on
  runtime conditions.
  """

  use Ecto.Schema

  @type t :: %__MODULE__{
          kind: String.t(),
          reason: String.t(),
          source_line: String.t(),
          source_function: String.t(),
          status: :resolved | :unresolved,
          fingerprint: String.t(),
          last_occurrence_at: DateTime.t(),
          muted: boolean()
        }

  schema "error_tracker_errors" do
    field :kind, :string
    field :reason, :string
    field :source_line, :string
    field :source_function, :string
    field :status, Ecto.Enum, values: [:resolved, :unresolved], default: :unresolved
    field :fingerprint, :binary
    field :last_occurrence_at, :utc_datetime_usec
    field :muted, :boolean

    has_many :occurrences, ErrorTracker.Occurrence

    timestamps(type: :utc_datetime_usec)
  end

  @doc false
  def new(kind, reason, %ErrorTracker.Stacktrace{} = stacktrace) do
    source = ErrorTracker.Stacktrace.source(stacktrace)

    {source_line, source_function} =
      if source do
        source_line = if source.line, do: "#{source.file}:#{source.line}", else: "(nofile)"
        source_function = "#{source.module}.#{source.function}/#{source.arity}"

        {source_line, source_function}
      else
        {"-", "-"}
      end

    params = [
      kind: to_string(kind),
      source_line: source_line,
      source_function: source_function
    ]

    fingerprint = :crypto.hash(:sha256, params |> Keyword.values() |> Enum.join())

    %__MODULE__{}
    |> Ecto.Changeset.change(params)
    |> Ecto.Changeset.put_change(:reason, reason)
    |> Ecto.Changeset.put_change(:fingerprint, Base.encode16(fingerprint))
    |> Ecto.Changeset.put_change(:last_occurrence_at, DateTime.utc_now())
    |> Ecto.Changeset.apply_action(:new)
  end

  @doc """
  Returns if the Error has information of the source or not.

  Errors usually have information about in which line and function occurred, but
  in some cases (like an Oban job ending with `{:error, any()}`) we cannot get
  that information and no source is stored.
  """
  def has_source_info?(%__MODULE__{source_function: "-", source_line: "-"}), do: false
  def has_source_info?(%__MODULE__{}), do: true
end


================================================
FILE: lib/error_tracker/schemas/occurrence.ex
================================================
defmodule ErrorTracker.Occurrence do
  @moduledoc """
  Schema to store a particular instance of an error in a given time.

  It contains all the metadata available about the moment and the environment
  in which the exception raised.
  """

  use Ecto.Schema

  import Ecto.Changeset

  require Logger

  @type t :: %__MODULE__{}

  schema "error_tracker_occurrences" do
    field :reason, :string

    field :context, :map
    field :breadcrumbs, {:array, :string}

    embeds_one :stacktrace, ErrorTracker.Stacktrace
    belongs_to :error, ErrorTracker.Error

    timestamps(type: :utc_datetime_usec, updated_at: false)
  end

  @doc false
  def changeset(occurrence, attrs) do
    occurrence
    |> cast(attrs, [:context, :reason, :breadcrumbs])
    |> maybe_put_stacktrace()
    |> validate_required([:reason, :stacktrace])
    |> validate_context()
    |> foreign_key_constraint(:error)
  end

  # This function validates if the context can be serialized to JSON before
  # storing it to the DB.
  #
  # If it cannot be serialized a warning log message is emitted and an error
  # is stored in the context.
  #
  defp validate_context(changeset) do
    if changeset.valid? do
      context = get_field(changeset, :context, %{})

      db_json_encoder =
        ErrorTracker.Repo.with_adapter(fn
          :postgres -> Application.get_env(:postgrex, :json_library)
          :mysql -> Application.get_env(:myxql, :json_library)
          :sqlite -> Application.get_env(:ecto_sqlite3, :json_library)
        end)

      validated_context =
        try do
          json_encoder = db_json_encoder || ErrorTracker.__default_json_encoder__()
          _iodata = json_encoder.encode_to_iodata!(context)

          context
        rescue
          _e ->
            Logger.warning("[ErrorTracker] Context has been ignored: it is not serializable to JSON.")

            %{
              error: "Context not stored because it contains information not serializable to JSON."
            }
        end

      put_change(changeset, :context, validated_context)
    else
      changeset
    end
  end

  defp maybe_put_stacktrace(changeset) do
    if stacktrace = Map.get(changeset.params, "stacktrace"),
      do: put_embed(changeset, :stacktrace, stacktrace),
      else: changeset
  end
end


================================================
FILE: lib/error_tracker/schemas/stacktrace.ex
================================================
defmodule ErrorTracker.Stacktrace do
  @moduledoc """
  An Stacktrace contains the information about the execution stack for a given
  occurrence of an exception.
  """

  use Ecto.Schema

  @type t :: %__MODULE__{}

  @primary_key false
  embedded_schema do
    embeds_many :lines, Line, primary_key: false do
      field :application, :string
      field :module, :string
      field :function, :string
      field :arity, :integer
      field :file, :string
      field :line, :integer
    end
  end

  def new(stack) do
    lines_params =
      for {module, function, arity, opts} <- stack do
        application = Application.get_application(module)

        %{
          application: to_string(application),
          module: module |> to_string() |> String.replace_prefix("Elixir.", ""),
          function: to_string(function),
          arity: normalize_arity(arity),
          file: to_string(opts[:file]),
          line: opts[:line]
        }
      end

    %__MODULE__{}
    |> Ecto.Changeset.cast(%{lines: lines_params}, [])
    |> Ecto.Changeset.cast_embed(:lines, with: &line_changeset/2)
    |> Ecto.Changeset.apply_action(:new)
  end

  defp normalize_arity(a) when is_integer(a), do: a
  defp normalize_arity(a) when is_list(a), do: length(a)

  defp line_changeset(%__MODULE__.Line{} = line, params) do
    Ecto.Changeset.cast(line, params, ~w[application module function arity file line]a)
  end

  @doc """
  Source of the error stack trace.

  The first line matching the client application. If no line belongs to the current
  application, just the first line.
  """
  def source(%__MODULE__{} = stack) do
    client_app = :error_tracker |> Application.fetch_env!(:otp_app) |> to_string()

    Enum.find(stack.lines, &(&1.application == client_app)) || List.first(stack.lines)
  end
end

defimpl String.Chars, for: ErrorTracker.Stacktrace do
  def to_string(%ErrorTracker.Stacktrace{} = stack) do
    Enum.join(stack.lines, "\n")
  end
end

defimpl String.Chars, for: ErrorTracker.Stacktrace.Line do
  def to_string(%ErrorTracker.Stacktrace.Line{} = stack_line) do
    "#{stack_line.module}.#{stack_line.function}/#{stack_line.arity} in #{stack_line.file}:#{stack_line.line}"
  end
end


================================================
FILE: lib/error_tracker/telemetry.ex
================================================
defmodule ErrorTracker.Telemetry do
  @moduledoc """
  Telemetry events of ErrorTracker.

  ErrorTracker emits some events to allow third parties to receive information
  of errors and occurrences stored.

  ### Error events

  Those occur during the life cycle of an error:

  * `[:error_tracker, :error, :new]`: is emitted when a new error is stored and
  no previous occurrences were known.

  * `[:error_tracker, :error, :resolved]`: is emitted when a new error is marked
  as resolved on the UI.

  * `[:error_tracker, :error, :unresolved]`: is emitted when a new error is
  marked as unresolved on the UI or a new occurrence is registered, moving the
  error to the unresolved state.

  ### Occurrence events

  There is only one event emitted for occurrences:

  * `[:error_tracker, :occurrence, :new]`: is emitted when a new occurrence is
  stored.

  ### Measures and metadata

  Each event is emitted with some measures and metadata, which can be used to
  receive information without having to query the database again:

  | event                                   | measures       | metadata                          |
  | --------------------------------------- | -------------- | ----------------------------------|
  | `[:error_tracker, :error, :new]`        | `:system_time` | `:error`                          |
  | `[:error_tracker, :error, :unresolved]` | `:system_time` | `:error`                          |
  | `[:error_tracker, :error, :resolved]`   | `:system_time` | `:error`                          |
  | `[:error_tracker, :occurrence, :new]`   | `:system_time` | `:occurrence`, `:error`, `:muted` |

  The metadata keys contain the following data:

  * `:error` - An `%ErrorTracker.Error{}` struct representing the error.
  * `:occurrence` - An `%ErrorTracker.Occurrence{}` struct representing the occurrence.
  * `:muted` - A boolean indicating whether the error is muted or not.
  """

  @doc false
  def new_error(%ErrorTracker.Error{} = error) do
    measurements = %{system_time: System.system_time()}
    metadata = %{error: error}
    :telemetry.execute([:error_tracker, :error, :new], measurements, metadata)
  end

  @doc false
  def unresolved_error(%ErrorTracker.Error{} = error) do
    measurements = %{system_time: System.system_time()}
    metadata = %{error: error}
    :telemetry.execute([:error_tracker, :error, :unresolved], measurements, metadata)
  end

  @doc false
  def resolved_error(%ErrorTracker.Error{} = error) do
    measurements = %{system_time: System.system_time()}
    metadata = %{error: error}
    :telemetry.execute([:error_tracker, :error, :resolved], measurements, metadata)
  end

  @doc false
  def new_occurrence(%ErrorTracker.Occurrence{} = occurrence, muted) when is_boolean(muted) do
    measurements = %{system_time: System.system_time()}
    metadata = %{error: occurrence.error, occurrence: occurrence, muted: muted}
    :telemetry.execute([:error_tracker, :occurrence, :new], measurements, metadata)
  end
end


================================================
FILE: lib/error_tracker/web/components/core_components.ex
================================================
defmodule ErrorTracker.Web.CoreComponents do
  @moduledoc false
  use Phoenix.Component

  @doc """
  Renders a button.

  ## Examples

      <.button>Send!</.button>
      <.button phx-click="go" class="ml-2">Send!</.button>
  """
  attr :type, :string, default: nil
  attr :class, :string, default: nil
  attr :rest, :global, include: ~w(disabled form name value href patch navigate)

  slot :inner_block, required: true

  def button(%{type: "link"} = assigns) do
    ~H"""
    <.link
      class={[
        "phx-submit-loading:opacity-75 py-[11.5px]",
        "text-sm font-semibold text-sky-500 hover:text-white/80",
        @class
      ]}
      {@rest}
    >
      {render_slot(@inner_block)}
    </.link>
    """
  end

  def button(assigns) do
    ~H"""
    <button
      type={@type}
      class={[
        "phx-submit-loading:opacity-75 rounded-lg bg-sky-500 hover:bg-sky-700 py-2 px-4",
        "text-sm text-white active:text-white/80",
        @class
      ]}
      {@rest}
    >
      {render_slot(@inner_block)}
    </button>
    """
  end

  @doc """
  Renders a badge.

  ## Examples

      <.badge>Info</.badge>
      <.badge color={:red}>Error</.badge>
  """
  attr :color, :atom, default: :blue
  attr :rest, :global

  slot :inner_block, required: true

  def badge(assigns) do
    color_class =
      case assigns.color do
        :blue -> "bg-blue-900 text-blue-300"
        :gray -> "bg-gray-700 text-gray-300"
        :red -> "bg-red-400/10 text-red-300 ring-red-400/20"
        :green -> "bg-emerald-400/10 text-emerald-300 ring-emerald-400/20"
        :yellow -> "bg-yellow-900 text-yellow-300"
        :indigo -> "bg-indigo-900 text-indigo-300"
        :purple -> "bg-purple-900 text-purple-300"
        :pink -> "bg-pink-900 text-pink-300"
      end

    assigns = Map.put(assigns, :color_class, color_class)

    ~H"""
    <span
      class={["text-sm font-medium me-2 py-1 px-2 rounded-lg ring-1 ring-inset", @color_class]}
      {@rest}
    >
      {render_slot(@inner_block)}
    </span>
    """
  end

  attr :page, :integer, required: true
  attr :total_pages, :integer, required: true
  attr :event_previous, :string, default: "prev-page"
  attr :event_next, :string, default: "next-page"

  def pagination(assigns) do
    ~H"""
    <div class="mt-10 w-full flex">
      <button
        :if={@page > 1}
        class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400  bg-gray-900 border border-gray-400 rounded-lg hover:bg-gray-800 hover:text-white"
        phx-click={@event_previous}
      >
        Previous page
      </button>
      <button
        :if={@page < @total_pages}
        class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 bg-gray-900 border border-gray-400 rounded-lg hover:bg-gray-800 hover:text-white"
        phx-click={@event_next}
      >
        Next page
      </button>
    </div>
    """
  end

  attr :title, :string
  attr :title_class, :string, default: nil
  attr :rest, :global

  slot :inner_block, required: true

  def section(assigns) do
    ~H"""
    <div>
      <h2
        :if={assigns[:title]}
        class={["text-sm font-semibold mb-2 uppercase text-gray-400", @title_class]}
      >
        {@title}
      </h2>
      {render_slot(@inner_block)}
    </div>
    """
  end

  attr :name, :string, values: ~w[bell bell-slash arrow-left arrow-right]

  def icon(%{name: "bell"} = assigns) do
    ~H"""
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 16 16"
      fill="currentColor"
      class="!h-4 !w-4 inline-block"
    >
      <path
        fill-rule="evenodd"
        d="M12 5a4 4 0 0 0-8 0v2.379a1.5 1.5 0 0 1-.44 1.06L2.294 9.707a1 1 0 0 0-.293.707V11a1 1 0 0 0 1 1h2a3 3 0 1 0 6 0h2a1 1 0 0 0 1-1v-.586a1 1 0 0 0-.293-.707L12.44 8.44A1.5 1.5 0 0 1 12 7.38V5Zm-5.5 7a1.5 1.5 0 0 0 3 0h-3Z"
        clip-rule="evenodd"
      />
    </svg>
    """
  end

  def icon(%{name: "bell-slash"} = assigns) do
    ~H"""
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 16 16"
      fill="currentColor"
      class="!h-4 !w-4 inline-block"
    >
      <path
        fill-rule="evenodd"
        d="M4 7.379v-.904l6.743 6.742A3 3 0 0 1 5 12H3a1 1 0 0 1-1-1v-.586a1 1 0 0 1 .293-.707L3.56 8.44A1.5 1.5 0 0 0 4 7.38ZM6.5 12a1.5 1.5 0 0 0 3 0h-3Z"
        clip-rule="evenodd"
      />
      <path d="M14 11a.997.997 0 0 1-.096.429L4.92 2.446A4 4 0 0 1 12 5v2.379c0 .398.158.779.44 1.06l1.267 1.268a1 1 0 0 1 .293.707V11ZM2.22 2.22a.75.75 0 0 1 1.06 0l10.5 10.5a.75.75 0 1 1-1.06 1.06L2.22 3.28a.75.75 0 0 1 0-1.06Z" />
    </svg>
    """
  end

  def icon(%{name: "arrow-left"} = assigns) do
    ~H"""
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 16 16"
      fill="currentColor"
      class="!h-4 !w-4 inline-block"
    >
      <path
        fill-rule="evenodd"
        d="M14 8a.75.75 0 0 1-.75.75H4.56l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 1.06L4.56 7.25h8.69A.75.75 0 0 1 14 8Z"
        clip-rule="evenodd"
      />
    </svg>
    """
  end

  def icon(%{name: "arrow-right"} = assigns) do
    ~H"""
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 16 16"
      fill="currentColor"
      class="!h-4 !w-4 inline-block"
    >
      <path
        fill-rule="evenodd"
        d="M2 8a.75.75 0 0 1 .75-.75h8.69L8.22 4.03a.75.75 0 0 1 1.06-1.06l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06l3.22-3.22H2.75A.75.75 0 0 1 2 8Z"
        clip-rule="evenodd"
      />
    </svg>
    """
  end
end


================================================
FILE: lib/error_tracker/web/components/layouts/live.html.heex
================================================
<.navbar id="navbar" {assigns} />
<main class="container px-4 mx-auto mt-4 mb-4">
  {@inner_content}
</main>


================================================
FILE: lib/error_tracker/web/components/layouts/root.html.heex
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <meta name="live-path" content={get_socket_config(:path)} />
    <meta name="live-transport" content={get_socket_config(:transport)} />

    <title>{assigns[:page_title] || "🐛 ErrorTracker"}</title>

    <style nonce={@csp_nonces[:style]}>
      <%= raw get_content(:css) %>
    </style>
    <script nonce={@csp_nonces[:script]}>
      <%= raw get_content(:js) %>
    </script>
  </head>

  <body class="bg-gray-800 text-white">
    {@inner_content}
  </body>
</html>


================================================
FILE: lib/error_tracker/web/components/layouts.ex
================================================
defmodule ErrorTracker.Web.Layouts do
  @moduledoc false
  use ErrorTracker.Web, :html

  phoenix_js_paths =
    for app <- ~w[phoenix phoenix_html phoenix_live_view]a do
      path = Application.app_dir(app, ["priv", "static", "#{app}.js"])
      Module.put_attribute(__MODULE__, :external_resource, path)
      path
    end

  @static_path Application.app_dir(:error_tracker, ["priv", "static"])
  @external_resource css_path = Path.join(@static_path, "app.css")
  @external_resource js_path = Path.join(@static_path, "app.js")

  @css File.read!(css_path)

  @js """
  #{for path <- phoenix_js_paths, do: path |> File.read!() |> String.replace("//# sourceMappingURL=", "// ")}
  #{File.read!(js_path)}
  """

  @default_socket_config %{path: "/live", transport: :websocket}

  embed_templates "layouts/*"

  def get_content(:css), do: @css
  def get_content(:js), do: @js

  def get_socket_config(key) do
    default = Map.get(@default_socket_config, key)
    config = Application.get_env(:error_tracker, :live_view_socket, [])
    Keyword.get(config, key, default)
  end

  def navbar(assigns) do
    ~H"""
    <nav class="border-gray-400 bg-gray-900">
      <div class="container flex flex-wrap items-center justify-between mx-auto p-4">
        <.link
          href={dashboard_path(@socket)}
          class="self-center text-2xl font-semibold whitespace-nowrap text-white"
        >
          <span class="mr-2">🐛</span>ErrorTracker
        </.link>
        <button
          type="button"
          class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded -lg md:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-500"
          aria-controls="navbar-main"
          aria-expanded="false"
          phx-click={JS.toggle(to: "#navbar-main")}
        >
          <span class="sr-only">Open main menu</span>
          <svg
            class="w-5 h-5"
            aria-hidden="true"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 17 14"
          >
            <path
              stroke="currentColor"
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M1 1h15M1 7h15M1 13h15"
            />
          </svg>
        </button>
        <div class="hidden w-full md:block md:w-auto" id="navbar-main">
          <ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-400 bg-gray-900 rounded-lg md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-gray-800">
            <.navbar_item to="https://github.com/elixir-error-tracker/error-tracker" target="_blank">
              <svg
                width="18"
                height="18"
                aria-hidden="true"
                viewBox="0 0 24 24"
                version="1.1"
                class="inline-block mr-1 align-text-top"
              >
                <path
                  fill="currentColor"
                  d="M12.5.75C6.146.75 1 5.896 1 12.25c0 5.089 3.292 9.387 7.863 10.91.575.101.79-.244.79-.546 0-.273-.014-1.178-.014-2.142-2.889.532-3.636-.704-3.866-1.35-.13-.331-.69-1.352-1.18-1.625-.402-.216-.977-.748-.014-.762.906-.014 1.553.834 1.769 1.179 1.035 1.74 2.688 1.25 3.349.948.1-.747.402-1.25.733-1.538-2.559-.287-5.232-1.279-5.232-5.678 0-1.25.445-2.285 1.178-3.09-.115-.288-.517-1.467.115-3.048 0 0 .963-.302 3.163 1.179.92-.259 1.897-.388 2.875-.388.977 0 1.955.13 2.875.388 2.2-1.495 3.162-1.179 3.162-1.179.633 1.581.23 2.76.115 3.048.733.805 1.179 1.825 1.179 3.09 0 4.413-2.688 5.39-5.247 5.678.417.36.776 1.05.776 2.128 0 1.538-.014 2.774-.014 3.162 0 .302.216.662.79.547C20.709 21.637 24 17.324 24 12.25 24 5.896 18.854.75 12.5.75Z"
                >
                </path>
              </svg>
              GitHub
            </.navbar_item>
          </ul>
        </div>
      </div>
    </nav>
    """
  end

  attr :to, :string, required: true
  attr :rest, :global

  slot :inner_block, required: true

  def navbar_item(assigns) do
    ~H"""
    <li>
      <a
        href={@to}
        class="whitespace-nowrap flex-0 block py-2 px-3 rounded-lg text-white hover:text-white hover:bg-gray-700 md:hover:bg-transparent md:border-0 md:hover:text-sky-500"
        {@rest}
      >
        {render_slot(@inner_block)}
      </a>
    </li>
    """
  end
end


================================================
FILE: lib/error_tracker/web/helpers.ex
================================================
defmodule ErrorTracker.Web.Helpers do
  @moduledoc false

  @doc false
  def sanitize_module(<<"Elixir.", str::binary>>), do: str
  def sanitize_module(str), do: str

  @doc false
  def format_datetime(%DateTime{} = dt), do: Calendar.strftime(dt, "%c %Z")
end


================================================
FILE: lib/error_tracker/web/hooks/set_assigns.ex
================================================
defmodule ErrorTracker.Web.Hooks.SetAssigns do
  @moduledoc false

  import Phoenix.Component, only: [assign: 2]

  def on_mount({:set_dashboard_path, path}, _params, session, socket) do
    socket = %{socket | private: Map.put(socket.private, :dashboard_path, path)}

    {:cont, assign(socket, csp_nonces: session["csp_nonces"])}
  end
end


================================================
FILE: lib/error_tracker/web/live/dashboard.ex
================================================
defmodule ErrorTracker.Web.Live.Dashboard do
  @moduledoc false

  use ErrorTracker.Web, :live_view

  import Ecto.Query

  alias ErrorTracker.Error
  alias ErrorTracker.Repo
  alias ErrorTracker.Web.Search

  @per_page 10

  @impl Phoenix.LiveView
  def handle_params(params, uri, socket) do
    path = struct(URI, uri |> URI.parse() |> Map.take([:path, :query]))

    {:noreply,
     socket
     |> assign(
       path: path,
       search: Search.from_params(params),
       page: 1,
       search_form: Search.to_form(params)
     )
     |> paginate_errors()}
  end

  @impl Phoenix.LiveView
  def handle_event("search", params, socket) do
    search = Search.from_params(params["search"] || %{})

    %URI{} = path = socket.assigns.path
    path_w_filters = %{path | query: URI.encode_query(search)}

    {:noreply, push_patch(socket, to: URI.to_string(path_w_filters))}
  end

  @impl Phoenix.LiveView
  def handle_event("next-page", _params, socket) do
    {:noreply, socket |> assign(page: socket.assigns.page + 1) |> paginate_errors()}
  end

  @impl Phoenix.LiveView
  def handle_event("prev-page", _params, socket) do
    {:noreply, socket |> assign(page: socket.assigns.page - 1) |> paginate_errors()}
  end

  @impl Phoenix.LiveView
  def handle_event("resolve", %{"error_id" => id}, socket) do
    error = Repo.get(Error, id)
    {:ok, _resolved} = ErrorTracker.resolve(error)

    {:noreply, paginate_errors(socket)}
  end

  @impl Phoenix.LiveView
  def handle_event("unresolve", %{"error_id" => id}, socket) do
    error = Repo.get(Error, id)
    {:ok, _unresolved} = ErrorTracker.unresolve(error)

    {:noreply, paginate_errors(socket)}
  end

  @impl Phoenix.LiveView
  def handle_event("mute", %{"error_id" => id}, socket) do
    error = Repo.get(Error, id)
    {:ok, _muted} = ErrorTracker.mute(error)

    {:noreply, paginate_errors(socket)}
  end

  @impl Phoenix.LiveView
  def handle_event("unmute", %{"error_id" => id}, socket) do
    error = Repo.get(Error, id)
    {:ok, _unmuted} = ErrorTracker.unmute(error)

    {:noreply, paginate_errors(socket)}
  end

  defp paginate_errors(socket) do
    %{page: page, search: search} = socket.assigns
    offset = (page - 1) * @per_page
    query = filter(Error, search)

    total_errors = Repo.aggregate(query, :count)

    errors =
      Repo.all(
        from query,
          order_by: [desc: :last_occurrence_at],
          offset: ^offset,
          limit: @per_page
      )

    error_ids = Enum.map(errors, & &1.id)

    occurrences =
      if errors == [] do
        []
      else
        errors
        |> Ecto.assoc(:occurrences)
        |> where([o], o.error_id in ^error_ids)
        |> group_by([o], o.error_id)
        |> select([o], {o.error_id, count(o.id)})
        |> Repo.all()
      end

    assign(socket,
      errors: errors,
      occurrences: Map.new(occurrences),
      total_pages: (total_errors / @per_page) |> Float.ceil() |> trunc()
    )
  end

  defp filter(query, search) do
    Enum.reduce(search, query, &do_filter/2)
  end

  defp do_filter({:status, status}, query) do
    where(query, [error], error.status == ^status)
  end

  defp do_filter({field, value}, query) do
    # Postgres provides the ILIKE operator which produces a case-insensitive match between two
    # strings. SQLite3 only supports LIKE, which is case-insensitive for ASCII characters.
    Repo.with_adapter(fn
      :postgres -> where(query, [error], ilike(field(error, ^field), ^"%#{value}%"))
      :mysql -> where(query, [error], like(field(error, ^field), ^"%#{value}%"))
      :sqlite -> where(query, [error], like(field(error, ^field), ^"%#{value}%"))
    end)
  end
end


================================================
FILE: lib/error_tracker/web/live/dashboard.html.heex
================================================
<.form
  for={@search_form}
  id="search"
  class="mb-4 text-black grid md:grid-cols-4 grid-cols-2 gap-2"
  phx-change="search"
>
  <input
    name={@search_form[:reason].name}
    value={@search_form[:reason].value}
    type="text"
    placeholder="Error"
    class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
    phx-debounce
  />
  <input
    name={@search_form[:source_line].name}
    value={@search_form[:source_line].value}
    type="text"
    placeholder="Source line"
    class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
    phx-debounce
  />
  <input
    name={@search_form[:source_function].name}
    value={@search_form[:source_function].value}
    type="text"
    placeholder="Source function"
    class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
    phx-debounce
  />
  <select
    name={@search_form[:status].name}
    class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
  >
    <option value="" selected={@search_form[:status].value == ""}>All</option>
    <option value="unresolved" selected={@search_form[:status].value == "unresolved"}>
      Unresolved
    </option>
    <option value="resolved" selected={@search_form[:status].value == "resolved"}>
      Resolved
    </option>
  </select>
</.form>

<div class="relative overflow-x-auto shadow-md sm:rounded-lg ring-1 ring-gray-900">
  <table class="w-full text-sm text-left rtl:text-right text-gray-400 table-fixed">
    <thead class="text-xs uppercase bg-gray-900">
      <tr>
        <th scope="col" class="px-4 pr-2 w-72">Error</th>
        <th scope="col" class="px-4 py-3 w-72">Occurrences</th>
        <th scope="col" class="px-4 py-3 w-28">Status</th>
        <th scope="col" class="px-4 py-3 w-28"></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td :if={@errors == []} colspan="4" class="text-center py-8 font-extralight">
          No errors to show 🎉
        </td>
      </tr>
      <tr
        :for={error <- @errors}
        class="border-b bg-gray-400/10 border-y border-gray-900 hover:bg-gray-800/60 last-of-type:border-b-0"
      >
        <td scope="row" class="px-4 py-4 font-medium text-white relative">
          <.link navigate={error_path(@socket, error, @search)} class="absolute inset-1">
            <span class="sr-only">({sanitize_module(error.kind)}) {error.reason}</span>
          </.link>
          <p class="whitespace-nowrap text-ellipsis overflow-hidden">
            ({sanitize_module(error.kind)}) {error.reason}
          </p>
          <p
            :if={ErrorTracker.Error.has_source_info?(error)}
            class="whitespace-nowrap text-ellipsis overflow-hidden font-normal text-gray-400"
          >
            {sanitize_module(error.source_function)}
            <br />
            {error.source_line}
          </p>
        </td>
        <td class="px-4 py-4">
          <p>Last: {format_datetime(error.last_occurrence_at)}</p>
          <p>Total: {@occurrences[error.id]}</p>
        </td>
        <td class="px-4 py-4">
          <.badge :if={error.status == :resolved} color={:green}>Resolved</.badge>
          <.badge :if={error.status == :unresolved} color={:red}>Unresolved</.badge>
        </td>
        <td class="px-4 py-4 text-center">
          <div class="flex justify-between">
            <.button
              :if={error.status == :unresolved}
              phx-click="resolve"
              phx-value-error_id={error.id}
            >
              Resolve
            </.button>

            <.button
              :if={error.status == :resolved}
              phx-click="unresolve"
              phx-value-error_id={error.id}
            >
              Unresolve
            </.button>

            <.button :if={!error.muted} phx-click="mute" type="link" phx-value-error_id={error.id}>
              <.icon name="bell-slash" /> Mute
            </.button>

            <.button
              :if={error.muted}
              phx-click="unmute"
              type="link"
              phx-value-error_id={error.id}
            >
              <.icon name="bell" /> Unmute
            </.button>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>

<.pagination page={@page} total_pages={@total_pages} />


================================================
FILE: lib/error_tracker/web/live/show.ex
================================================
defmodule ErrorTracker.Web.Live.Show do
  @moduledoc false
  use ErrorTracker.Web, :live_view

  import Ecto.Query

  alias ErrorTracker.Error
  alias ErrorTracker.Occurrence
  alias ErrorTracker.Repo
  alias ErrorTracker.Web.Search

  @occurrences_to_navigate 50

  @impl Phoenix.LiveView
  def mount(%{"id" => id} = params, _session, socket) do
    error = Repo.get!(Error, id)

    {:ok,
     assign(socket,
       error: error,
       app: Application.fetch_env!(:error_tracker, :otp_app),
       search: Search.from_params(params)
     )}
  end

  @impl Phoenix.LiveView
  def handle_params(params, _uri, socket) do
    occurrence =
      if occurrence_id = params["occurrence_id"] do
        socket.assigns.error
        |> Ecto.assoc(:occurrences)
        |> Repo.get!(occurrence_id)
      else
        socket.assigns.error
        |> Ecto.assoc(:occurrences)
        |> order_by([o], desc: o.id)
        |> limit(1)
        |> Repo.one()
      end

    socket =
      socket
      |> assign(occurrence: occurrence)
      |> load_related_occurrences()

    {:noreply, socket}
  end

  @impl Phoenix.LiveView
  def handle_event("occurrence_navigation", %{"occurrence_id" => id}, socket) do
    occurrence_path =
      occurrence_path(
        socket,
        %Occurrence{error_id: socket.assigns.error.id, id: id},
        socket.assigns.search
      )

    {:noreply, push_patch(socket, to: occurrence_path)}
  end

  @impl Phoenix.LiveView
  def handle_event("resolve", _params, socket) do
    {:ok, updated_error} = ErrorTracker.resolve(socket.assigns.error)

    {:noreply, assign(socket, :error, updated_error)}
  end

  @impl Phoenix.LiveView
  def handle_event("unresolve", _params, socket) do
    {:ok, updated_error} = ErrorTracker.unresolve(socket.assigns.error)

    {:noreply, assign(socket, :error, updated_error)}
  end

  @impl Phoenix.LiveView
  def handle_event("mute", _params, socket) do
    {:ok, updated_error} = ErrorTracker.mute(socket.assigns.error)

    {:noreply, assign(socket, :error, updated_error)}
  end

  @impl Phoenix.LiveView
  def handle_event("unmute", _params, socket) do
    {:ok, updated_error} = ErrorTracker.unmute(socket.assigns.error)

    {:noreply, assign(socket, :error, updated_error)}
  end

  defp load_related_occurrences(socket) do
    current_occurrence = socket.assigns.occurrence
    base_query = Ecto.assoc(socket.assigns.error, :occurrences)

    half_limit = floor(@occurrences_to_navigate / 2)

    previous_occurrences_query = where(base_query, [o], o.id < ^current_occurrence.id)
    next_occurrences_query = where(base_query, [o], o.id > ^current_occurrence.id)
    previous_count = Repo.aggregate(previous_occurrences_query, :count)
    next_count = Repo.aggregate(next_occurrences_query, :count)

    {previous_limit, next_limit} =
      cond do
        previous_count < half_limit and next_count < half_limit ->
          {previous_count, next_count}

        previous_count < half_limit ->
          {previous_count, @occurrences_to_navigate - previous_count - 1}

        next_count < half_limit ->
          {@occurrences_to_navigate - next_count - 1, next_count}

        true ->
          {half_limit, half_limit}
      end

    occurrences =
      [
        related_occurrences(next_occurrences_query, next_limit),
        current_occurrence,
        related_occurrences(previous_occurrences_query, previous_limit)
      ]
      |> List.flatten()
      |> Enum.reverse()

    total_occurrences =
      socket.assigns.error
      |> Ecto.assoc(:occurrences)
      |> Repo.aggregate(:count)

    next_occurrence =
      base_query
      |> where([o], o.id > ^current_occurrence.id)
      |> order_by([o], asc: o.id)
      |> limit(1)
      |> select([:id, :error_id, :inserted_at])
      |> Repo.one()

    prev_occurrence =
      base_query
      |> where([o], o.id < ^current_occurrence.id)
      |> order_by([o], desc: o.id)
      |> limit(1)
      |> select([:id, :error_id, :inserted_at])
      |> Repo.one()

    socket
    |> assign(:occurrences, occurrences)
    |> assign(:total_occurrences, total_occurrences)
    |> assign(:next, next_occurrence)
    |> assign(:prev, prev_occurrence)
  end

  defp related_occurrences(query, num_results) do
    query
    |> order_by([o], desc: o.id)
    |> select([:id, :error_id, :inserted_at])
    |> limit(^num_results)
    |> Repo.all()
  end
end


================================================
FILE: lib/error_tracker/web/live/show.html.heex
================================================
<div class="my-6">
  <.link navigate={dashboard_path(@socket, @search)}>
    <.icon name="arrow-left" /> Back to the dashboard
  </.link>
</div>

<div id="header">
  <p class="text-sm uppercase font-semibold text-gray-400">
    Error #{@error.id} @ {format_datetime(@occurrence.inserted_at)}
  </p>
  <h1 class="my-1 text-2xl w-full font-semibold whitespace-nowrap text-ellipsis overflow-hidden">
    ({sanitize_module(@error.kind)}) {@error.reason
    |> String.replace("\n", " ")
    |> String.trim()}
  </h1>
</div>

<div class="grid grid-cols-1 md:grid-cols-4 md:space-x-3 mt-6 gap-2">
  <div class="md:col-span-3 md:border-r md:border-gray-600 space-y-8 pr-5">
    <.section title="Full message">
      <pre class="overflow-auto p-4 rounded-lg bg-gray-300/10 border border-gray-900"><%= @occurrence.reason %></pre>
    </.section>

    <.section :if={ErrorTracker.Error.has_source_info?(@error)} title="Source">
      <pre class="overflow-auto text-sm p-4 rounded-lg bg-gray-300/10 border border-gray-900">
        <%= sanitize_module(@error.source_function) %>
        <%= @error.source_line %></pre>
    </.section>

    <.section :if={@occurrence.breadcrumbs != []} title="Bread crumbs">
      <div class="relative overflow-x-auto shadow-md sm:rounded-lg ring-1 ring-gray-900">
        <table class="w-full text-sm text-gray-400 table-fixed">
          <tr
            :for={
              {breadcrumb, index} <-
                @occurrence.breadcrumbs |> Enum.reverse() |> Enum.with_index()
            }
            class="border-b bg-gray-400/10 border-gray-900 last:border-b-0"
          >
            <td class="w-11 pl-2 py-4 font-medium text-white relative text-right">
              {length(@occurrence.breadcrumbs) - index}.
            </td>
            <td class="px-2 py-4 font-medium text-white relative">{breadcrumb}</td>
          </tr>
        </table>
      </div>
    </.section>

    <.section :if={@occurrence.stacktrace.lines != []} title="Stacktrace">
      <div class="p-4 bg-gray-300/10 border border-gray-900 rounded-lg">
        <div class="w-full mb-4">
          <label class="flex justify-end">
            <input
              type="checkbox"
              id="show-app-frames"
              class="ml-2 mr-2 mb-1 mt-1 inline-block text-sky-600 rounded focus:ring-sky-600 ring-offset-gray-800 focus:ring-2 bg-gray-700 border-gray-600"
              phx-click={JS.toggle(to: "#stacktrace tr:not([data-app=#{@app}])")}
            />
            <span class="text-md inline-block">
              Show only app frames
            </span>
          </label>
        </div>

        <div class="overflow-auto">
          <table class="w-100 text-sm" id="stacktrace">
            <tbody>
              <tr :for={line <- @occurrence.stacktrace.lines} data-app={line.application || @app}>
                <td class="px-2 align-top"><pre>(<%= line.application || @app %>)</pre></td>
                <td>
                  <pre><%= "#{sanitize_module(line.module)}.#{line.function}/#{line.arity}" %>
                <%= if line.line, do: "#{line.file}:#{line.line}", else: "(nofile)" %></pre>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </.section>

    <.section title="Context">
      <pre
        id="context"
        class="overflow-auto text-sm p-4 rounded-lg bg-gray-300/10 border border-gray-900"
        phx-hook="JsonPrettyPrint"
      >
        <%= ErrorTracker.__default_json_encoder__().encode_to_iodata!(@occurrence.context) %>
      </pre>
    </.section>
  </div>

  <div class="px-3 md:pl-0 space-y-8">
    <.section title={"Occurrence (#{@total_occurrences} total)"}>
      <form phx-change="occurrence_navigation">
        <select
          name="occurrence_id"
          class="w-full border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
        >
          <option
            :for={occurrence <- @occurrences}
            value={occurrence.id}
            selected={occurrence.id == @occurrence.id}
          >
            {format_datetime(occurrence.inserted_at)}
          </option>
        </select>
      </form>

      <nav class="grid grid-cols-2 gap-2 mt-2">
        <div class="text-left">
          <.link :if={@prev} patch={occurrence_path(@socket, @prev, @search)}>
            <.icon name="arrow-left" /> Prev
          </.link>
        </div>
        <div class="text-right">
          <.link :if={@next} patch={occurrence_path(@socket, @next, @search)}>
            Next <.icon name="arrow-right" />
          </.link>
        </div>
      </nav>
    </.section>

    <.section title="Error kind">
      <pre><%= sanitize_module(@error.kind) %></pre>
    </.section>

    <.section title="Last seen">
      <pre><%= format_datetime(@error.last_occurrence_at) %></pre>
    </.section>

    <.section title="First seen">
      <pre><%= format_datetime(@error.inserted_at) %></pre>
    </.section>

    <.section title="Status" title_class="mb-3">
      <.badge :if={@error.status == :resolved} color={:green}>Resolved</.badge>
      <.badge :if={@error.status == :unresolved} color={:red}>Unresolved</.badge>
    </.section>

    <.section>
      <div class="flex flex-col gap-y-4">
        <.button :if={@error.status == :unresolved} phx-click="resolve">
          Mark as resolved
        </.button>

        <.button :if={@error.status == :resolved} phx-click="unresolve">
          Mark as unresolved
        </.button>

        <.button :if={!@error.muted} phx-click="mute" type="link">
          <.icon name="bell-slash" /> Mute
        </.button>

        <.button :if={@error.muted} phx-click="unmute" type="link">
          <.icon name="bell" /> Unmute
        </.button>
      </div>
    </.section>
  </div>
</div>


================================================
FILE: lib/error_tracker/web/router/routes.ex
================================================
defmodule ErrorTracker.Web.Router.Routes do
  @moduledoc false

  alias ErrorTracker.Error
  alias ErrorTracker.Occurrence
  alias Phoenix.LiveView.Socket

  @doc """
  Returns the dashboard path
  """
  def dashboard_path(%Socket{} = socket, params \\ %{}) do
    socket
    |> dashboard_uri(params)
    |> URI.to_string()
  end

  @doc """
  Returns the path to see the details of an error
  """
  def error_path(%Socket{} = socket, %Error{id: id}, params \\ %{}) do
    socket
    |> dashboard_uri(params)
    |> URI.append_path("/#{id}")
    |> URI.to_string()
  end

  @doc """
  Returns the path to see the details of an occurrence
  """
  def occurrence_path(%Socket{} = socket, %Occurrence{id: id, error_id: error_id}, params \\ %{}) do
    socket
    |> dashboard_uri(params)
    |> URI.append_path("/#{error_id}/#{id}")
    |> URI.to_string()
  end

  defp dashboard_uri(%Socket{} = socket, params) do
    %URI{
      path: socket.private[:dashboard_path],
      query: if(Enum.any?(params), do: URI.encode_query(params))
    }
  end
end


================================================
FILE: lib/error_tracker/web/router.ex
================================================
defmodule ErrorTracker.Web.Router do
  @moduledoc """
  ErrorTracker UI integration into your application's router.
  """

  alias ErrorTracker.Web.Hooks.SetAssigns

  @doc """
  Creates the routes needed to use the `ErrorTracker` web interface.

  It requires a path in which you are going to serve the web interface.

  In order to work properly, the route should be in a scope with CSRF protection
  (usually the `:browser` pipeline).

  ## Security considerations

  The dashboard inlines both the JS and CSS assets. This means that, if your
  application has a Content Security Policy, you need to specify the
  `csp_nonce_assign_key` option, which is explained below.

  ## Options

  * `on_mount`: a list of mount hooks to use before invoking the dashboard
  LiveView views.

  * `as`: a session name to use for the dashboard LiveView session. By default
  it uses `:error_tracker_dashboard`.

  * `csp_nonce_assign_key`: an assign key to find the CSP nonce value used for assets.
  Supports either `atom()` or a map of type
  `%{optional(:img) => atom(), optional(:script) => atom(), optional(:style) => atom()}`
  """
  defmacro error_tracker_dashboard(path, opts \\ []) do
    quote bind_quoted: [path: path, opts: opts] do
      # Ensure that the given path includes previous scopes so we can generate proper
      # paths for navigating through the dashboard.
      scoped_path = Phoenix.Router.scoped_path(__MODULE__, path)
      # Generate the session name and session hooks.
      {session_name, session_opts} = ErrorTracker.Web.Router.__parse_options__(opts, scoped_path)

      scope path, alias: false, as: false do
        import Phoenix.LiveView.Router, only: [live: 4, live_session: 3]

        alias ErrorTracker.Web.Live.Show

        live_session session_name, session_opts do
          live "/", ErrorTracker.Web.Live.Dashboard, :index, as: session_name
          live "/:id", Show, :show, as: session_name
          live "/:id/:occurrence_id", Show, :show, as: session_name
        end
      end
    end
  end

  @doc false
  def __parse_options__(opts, path) do
    custom_on_mount = Keyword.get(opts, :on_mount, [])
    session_name = Keyword.get(opts, :as, :error_tracker_dashboard)

    csp_nonce_assign_key =
      case opts[:csp_nonce_assign_key] do
        nil -> nil
        key when is_atom(key) -> %{img: key, style: key, script: key}
        keys when is_map(keys) -> Map.take(keys, [:img, :style, :script])
      end

    session_opts = [
      session: {__MODULE__, :__session__, [csp_nonce_assign_key]},
      on_mount: [{SetAssigns, {:set_dashboard_path, path}}] ++ custom_on_mount,
      root_layout: {ErrorTracker.Web.Layouts, :root}
    ]

    {session_name, session_opts}
  end

  @doc false
  def __session__(conn, csp_nonce_assign_key) do
    %{
      "csp_nonces" => %{
        img: conn.assigns[csp_nonce_assign_key[:img]],
        style: conn.assigns[csp_nonce_assign_key[:style]],
        script: conn.assigns[csp_nonce_assign_key[:script]]
      }
    }
  end
end


================================================
FILE: lib/error_tracker/web/search.ex
================================================
defmodule ErrorTracker.Web.Search do
  @moduledoc false

  @types %{
    reason: :string,
    source_line: :string,
    source_function: :string,
    status: :string
  }

  defp changeset(params) do
    Ecto.Changeset.cast({%{}, @types}, params, Map.keys(@types))
  end

  @spec from_params(map()) :: %{atom() => String.t()}
  def from_params(params) do
    params |> changeset() |> Ecto.Changeset.apply_changes()
  end

  @spec to_form(map()) :: Phoenix.HTML.Form.t()
  def to_form(params) do
    params |> changeset() |> Phoenix.Component.to_form(as: :search)
  end
end


================================================
FILE: lib/error_tracker/web.ex
================================================
defmodule ErrorTracker.Web do
  @moduledoc """
  ErrorTracker includes a dashboard to view and inspect errors that occurred
  on your application and are already stored in the database.

  In order to use it, you need to add the following to your Phoenix's
  `router.ex` file:

  ```elixir
  defmodule YourAppWeb.Router do
    use Phoenix.Router
    use ErrorTracker.Web, :router

    ...

    scope "/" do
      ...

      error_tracker_dashboard "/errors"
    end
  end
  ```

  This will add the routes needed for ErrorTracker's dashboard to work.

  **Note:** when adding the dashboard routes, make sure you do it in an scope that
  has CSRF protection (usually the `:browser` pipeline in most projects), as
  otherwise you may experience LiveView issues like crashes and redirections.

  ## Security considerations

  Errors may contain sensitive information, like IP addresses, users information
  or even passwords sent on forms!

  Securing your dashboard is an important part of integrating ErrorTracker on
  your project.

  In order to do so, we recommend implementing your own security mechanisms in
  the form of a mount hook and pass it to the `error_tracker_dashboard` macro
  using the `on_mount` option.

  You can find more details on
  `ErrorTracker.Web.Router.error_tracker_dashboard/2`.

  ### Static assets

  Static assets (CSS and JS) are inlined during the compilation. If you have
  a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
  be sure to allow inline styles and scripts.

  To do this, ensure that your `style-src` and `script-src` policies include the
  `unsafe-inline` value.

  ## LiveView socket options

  By default the library expects you to have your LiveView socket at `/live` and
  using `websocket` transport.

  If that's not the case, you can configure it adding the following
  configuration to your app's config files:

  ```elixir
  config :error_tracker,
    live_view_socket: [
      path: "/my-custom-live-path"
      transport: :longpoll # (accepted values are :longpoll or :websocket)
    ]
  ```
  """

  @doc false
  def html do
    quote do
      import Phoenix.Controller, only: [get_csrf_token: 0]

      unquote(html_helpers())
    end
  end

  @doc false
  def live_view do
    quote do
      use Phoenix.LiveView, layout: {ErrorTracker.Web.Layouts, :live}

      unquote(html_helpers())
    end
  end

  @doc false
  def live_component do
    quote do
      use Phoenix.LiveComponent

      unquote(html_helpers())
    end
  end

  @doc false
  def router do
    quote do
      import ErrorTracker.Web.Router
    end
  end

  defp html_helpers do
    quote do
      use Phoenix.Component

      import ErrorTracker.Web.CoreComponents
      import ErrorTracker.Web.Helpers
      import ErrorTracker.Web.Router.Routes
      import Phoenix.HTML
      import Phoenix.LiveView.Helpers

      alias Phoenix.LiveView.JS
    end
  end

  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
end


================================================
FILE: lib/error_tracker.ex
================================================
defmodule ErrorTracker do
  @moduledoc """
  En Elixir-based built-in error tracking solution.

  The main objectives behind this project are:

  * Provide a basic free error tracking solution: because tracking errors in
  your application should be a requirement for almost any project, and helps to
  provide quality and maintenance to your project.

  * Be easy to use: by providing plug-and-play integrations, documentation and a
  simple UI to manage your errors.

  * Be as minimalistic as possible: you just need a database to store errors and
  a Phoenix application if you want to inspect them via web. That's all.

  ## Requirements

  ErrorTracker requires Elixir 1.15+, Ecto 3.11+, Phoenix LiveView 0.19+, and
  PostgreSQL, MySQL/MariaDB or SQLite3 as database.

  ## Integrations

  We currently include integrations for what we consider the basic stack of
  an application: Phoenix, Plug, and Oban.

  However, we may continue working in adding support for more systems and
  libraries in the future if there is enough interest from the community.

  If you want to manually report an error, you can use the `ErrorTracker.report/3` function.

  ## Context

  Aside from the information about each exception (kind, message, stack trace...)
  we also store contexts.

  Contexts are arbitrary maps that allow you to store extra information about an
  exception to be able to reproduce it later.

  Each integration includes a default context with useful information they
  can gather, but aside from that, you can also add your own information. You can
  do this in a per-process basis or in a per-call basis (or both).

  There are some requirements on the type of data that can be included in the
  context, so we recommend taking a look at `set_context/1` documentation.

  **Per process**

  This allows you to set a general context for the current process such as a Phoenix
  request or an Oban job. For example, you could include the following code in your
  authentication Plug to automatically include the user ID in any error that is
  tracked during the Phoenix request handling.

  ```elixir
  ErrorTracker.set_context(%{user_id: conn.assigns.current_user.id})
  ```

  **Per call**

  As we had seen before, you can use `ErrorTracker.report/3` to manually report an
  error. The third parameter of this function is optional and allows you to include
  extra context that will be tracked along with the error.

  ## Breadcrumbs

  Aside from contextual information, it is sometimes useful to know in which points
  of your code the code was executed in a given request / process.

  Using breadcrumbs allows you to add that information to any error generated and
  stored on a given process / request. And if you are using `Ash` or `Splode` their
  exceptions' breadcrumbs will be automatically populated.

  If you want to add a breadcrumb in a point of your code you can do so:

  ```elixir
  ErrorTracker.add_breadcrumb("Executed my super secret code")
  ```

  Breadcrumbs can be viewed in the dashboard on the details page of an occurrence.
  """

  import Ecto.Query

  alias ErrorTracker.Error
  alias ErrorTracker.Occurrence
  alias ErrorTracker.Repo
  alias ErrorTracker.Telemetry

  @typedoc """
  A map containing the relevant context for a particular error.
  """
  @type context :: %{(String.t() | atom()) => any()}

  @typedoc """
  An `Exception` or a `{kind, payload}` tuple compatible with `Exception.normalize/3`.
  """
  @type exception :: Exception.t() | {:error, any()} | {Exception.non_error_kind(), any()}

  @doc """
  Report an exception to be stored.

  Returns the occurrence stored or `:noop` if the ErrorTracker is disabled by
  configuration the exception has not been stored.

  Aside from the exception, it is expected to receive the stack trace and,
  optionally, a context map which will be merged with the current process
  context.

  Keep in mind that errors that occur in Phoenix controllers, Phoenix LiveViews
  and Oban jobs are automatically reported. You will need this function only if you
  want to report custom errors.

  ```elixir
  try do
    # your code
  catch
    e ->
      ErrorTracker.report(e, __STACKTRACE__)
  end
  ```

  ## Exceptions

  Exceptions can be passed in three different forms:

  * An exception struct: the module of the exception is stored along with
  the exception message.

  * A `{kind, exception}` tuple in which case the information is converted to
  an Elixir exception (if possible) and stored.
  """

  @spec report(exception(), Exception.stacktrace(), context()) :: Occurrence.t() | :noop
  def report(exception, stacktrace, given_context \\ %{}) do
    {kind, reason} = normalize_exception(exception, stacktrace)
    {:ok, stacktrace} = ErrorTracker.Stacktrace.new(stacktrace)
    {:ok, error} = Error.new(kind, reason, stacktrace)
    context = Map.merge(get_context(), given_context)
    breadcrumbs = get_breadcrumbs() ++ exception_breadcrumbs(exception)

    if enabled?() && !ignored?(error, context) do
      sanitized_context = sanitize_context(context)

      upsert_error!(error, stacktrace, sanitized_context, breadcrumbs, reason)
    else
      :noop
    end
  end

  @doc """
  Marks an error as resolved.

  If an error is marked as resolved and it happens again, it will automatically
  appear as unresolved again.
  """
  @spec resolve(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
  def resolve(%Error{status: :unresolved} = error) do
    changeset = Ecto.Changeset.change(error, status: :resolved)

    with {:ok, updated_error} <- Repo.update(changeset) do
      Telemetry.resolved_error(updated_error)
      {:ok, updated_error}
    end
  end

  @doc """
  Marks an error as unresolved.
  """
  @spec unresolve(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
  def unresolve(%Error{status: :resolved} = error) do
    changeset = Ecto.Changeset.change(error, status: :unresolved)

    with {:ok, updated_error} <- Repo.update(changeset) do
      Telemetry.unresolved_error(updated_error)
      {:ok, updated_error}
    end
  end

  @doc """
  Mutes the error so new occurrences won't send telemetry events.

  When an error is muted:
  - New occurrences are still tracked and stored in the database
  - No telemetry events are emitted for new occurrences
  - You can still see the error and its occurrences in the web UI

  This is useful for noisy errors that you want to keep tracking but don't want to
  receive notifications about.
  """
  @spec mute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
  def mute(%Error{} = error) do
    changeset = Ecto.Changeset.change(error, muted: true)

    Repo.update(changeset)
  end

  @doc """
  Unmutes the error so new occurrences will send telemetry events again.

  This reverses the effect of `mute/1`, allowing telemetry events to be emitted
  for new occurrences of this error again.
  """
  @spec unmute(Error.t()) :: {:ok, Error.t()} | {:error, Ecto.Changeset.t()}
  def unmute(%Error{} = error) do
    changeset = Ecto.Changeset.change(error, muted: false)

    Repo.update(changeset)
  end

  @doc """
  Sets the current process context.

  The given context will be merged into the current process context. The given context
  may override existing keys from the current process context.

  ## Context depth

  You can store context on more than one level of depth, but take into account
  that the merge operation is performed on the first level.

  That means that any existing data on deep levels for he current context will
  be replaced if the first level key is received on the new contents.

  ## Content serialization

  The content stored on the context should be serializable using the JSON library used by the
  application (usually `JSON` for Elixir 1.18+ and `Jason` for older versions), so it is
  recommended to use primitive types (strings, numbers, booleans...).

  If you still need to pass more complex data types to your context, please test
  that they can be encoded to JSON or storing the errors will fail. You may need to define a
  custom encoder for that data type if not included by default.
  """
  @spec set_context(context()) :: context()
  def set_context(params) when is_map(params) do
    current_context = Process.get(:error_tracker_context, %{})

    Process.put(:error_tracker_context, Map.merge(current_context, params))

    params
  end

  @doc """
  Obtain the context of the current process.
  """
  @spec get_context() :: context()
  def get_context do
    Process.get(:error_tracker_context, %{})
  end

  @doc """
  Adds a breadcrumb to the current process.

  The new breadcrumb will be added as the most recent entry of the breadcrumbs
  list.

  ## Breadcrumbs limit

  Breadcrumbs are a powerful tool that allows to add an infinite number of
  entries. However, it is not recommended to store errors with an excessive
  amount of breadcrumbs.

  As they are stored as an array of strings under the hood, storing many
  entries per error can lead to some delays and using extra disk space on the
  database.
  """
  @spec add_breadcrumb(String.t()) :: list(String.t())
  def add_breadcrumb(breadcrumb) when is_binary(breadcrumb) do
    current_breadcrumbs = Process.get(:error_tracker_breadcrumbs, [])
    new_breadcrumbs = current_breadcrumbs ++ [breadcrumb]

    Process.put(:error_tracker_breadcrumbs, new_breadcrumbs)

    new_breadcrumbs
  end

  @doc """
  Obtain the breadcrumbs of the current process.
  """
  @spec get_breadcrumbs() :: list(String.t())
  def get_breadcrumbs do
    Process.get(:error_tracker_breadcrumbs, [])
  end

  defp enabled? do
    !!Application.get_env(:error_tracker, :enabled, true)
  end

  defp ignored?(error, context) do
    ignorer = Application.get_env(:error_tracker, :ignorer)

    ignorer && ignorer.ignore?(error, context)
  end

  defp sanitize_context(context) do
    filter_mod = Application.get_env(:error_tracker, :filter)

    if filter_mod,
      do: filter_mod.sanitize(context),
      else: context
  end

  defp normalize_exception(%struct{} = ex, _stacktrace) when is_exception(ex) do
    {to_string(struct), Exception.message(ex)}
  end

  defp normalize_exception({kind, ex}, stacktrace) do
    case Exception.normalize(kind, ex, stacktrace) do
      %struct{} = ex -> {to_string(struct), Exception.message(ex)}
      payload -> {to_string(kind), safe_to_string(payload)}
    end
  end

  defp safe_to_string(term) do
    to_string(term)
  rescue
    Protocol.UndefinedError ->
      inspect(term)
  end

  defp exception_breadcrumbs(exception) do
    case exception do
      {_kind, exception} -> exception_breadcrumbs(exception)
      %{bread_crumbs: breadcrumbs} -> breadcrumbs
      _other -> []
    end
  end

  defp upsert_error!(error, stacktrace, context, breadcrumbs, reason) do
    status_and_muted_query =
      from e in Error,
        where: [fingerprint: ^error.fingerprint],
        select: {e.status, e.muted}

    {existing_status, muted} =
      case Repo.one(status_and_muted_query) do
        {existing_status, muted} -> {existing_status, muted}
        nil -> {nil, false}
      end

    {:ok, {error, occurrence}} =
      Repo.transaction(fn ->
        error =
          Repo.with_adapter(fn
            :mysql ->
              Repo.insert!(error,
                on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]]
              )

            _other ->
              Repo.insert!(error,
                on_conflict: [set: [status: :unresolved, last_occurrence_at: DateTime.utc_now()]],
                conflict_target: :fingerprint
              )
          end)

        occurrence =
          error
          |> Ecto.build_assoc(:occurrences)
          |> Occurrence.changeset(%{
            stacktrace: stacktrace,
            context: context,
            breadcrumbs: breadcrumbs,
            reason: reason
          })
          |> Repo.insert!()

        {error, occurrence}
      end)

    %Occurrence{} = occurrence
    occurrence = %{occurrence | error: error}

    # If the error existed and was marked as resolved before this exception,
    # sent a Telemetry event
    # If it is a new error, sent a Telemetry event
    case existing_status do
      :resolved -> Telemetry.unresolved_error(error)
      :unresolved -> :noop
      nil -> Telemetry.new_error(error)
    end

    Telemetry.new_occurrence(occurrence, muted)
    occurrence
  end

  @default_json_encoder (cond do
                           Code.ensure_loaded?(JSON) ->
                             JSON

                           Code.ensure_loaded?(Jason) ->
                             Jason

                           true ->
                             raise """
                             No JSON encoder found. Please add Jason to your dependencies:

                                 {:jason, "~> 1.1"}

                             Or upgrade to Elixir 1.18+.
                             """
                         end)

  @doc false
  def __default_json_encoder__, do: @default_json_encoder
end


================================================
FILE: lib/mix/tasks/error_tracker.install.ex
================================================
defmodule Mix.Tasks.ErrorTracker.Install.Docs do
  @moduledoc false

  def short_doc do
    "Install and configure ErrorTracker for use in this application."
  end

  def example do
    "mix error_tracker.install"
  end

  def long_doc do
    """
    #{short_doc()}

    ## Example

    ```bash
    #{example()}
    ```
    """
  end
end

if Code.ensure_loaded?(Igniter) do
  defmodule Mix.Tasks.ErrorTracker.Install do
    @shortdoc "#{__MODULE__.Docs.short_doc()}"

    @moduledoc __MODULE__.Docs.long_doc()

    use Igniter.Mix.Task

    alias Igniter.Project.Config

    @impl Igniter.Mix.Task
    def info(_argv, _composing_task) do
      %Igniter.Mix.Task.Info{
        # Groups allow for overlapping arguments for tasks by the same author
        # See the generators guide for more.
        group: :error_tracker,
        # *other* dependencies to add
        # i.e `{:foo, "~> 2.0"}`
        adds_deps: [],
        # *other* dependencies to add and call their associated installers, if they exist
        # i.e `{:foo, "~> 2.0"}`
        installs: [],
        # An example invocation
        example: __MODULE__.Docs.example(),
        # A list of environments that this should be installed in.
        only: nil,
        # a list of positional arguments, i.e `[:file]`
        positional: [],
        # Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv
        # This ensures your option schema includes options from nested tasks
        composes: [],
        # `OptionParser` schema
        schema: [],
        # Default values for the options in the `schema`
        defaults: [],
        # CLI aliases
        aliases: [],
        # A list of options in the schema that are required
        required: []
      }
    end

    @impl Igniter.Mix.Task
    def igniter(igniter) do
      app_name = Igniter.Project.Application.app_name(igniter)
      {igniter, repo} = Igniter.Libs.Ecto.select_repo(igniter)
      {igniter, router} = Igniter.Libs.Phoenix.select_router(igniter)

      igniter
      |> set_up_configuration(app_name, repo)
      |> set_up_formatter()
      |> set_up_database(repo)
      |> set_up_web_ui(app_name, router)
    end

    defp set_up_configuration(igniter, app_name, repo) do
      igniter
      |> Config.configure_new("config.exs", :error_tracker, [:repo], repo)
      |> Config.configure_new("config.exs", :error_tracker, [:otp_app], app_name)
      |> Config.configure_new("config.exs", :error_tracker, [:enabled], true)
    end

    defp set_up_formatter(igniter) do
      Igniter.Project.Formatter.import_dep(igniter, :error_tracker)
    end

    defp set_up_database(igniter, repo) do
      migration_body = """
      def up, do: ErrorTracker.Migration.up()
      def down, do: ErrorTracker.Migration.down(version: 1)
      """

      Igniter.Libs.Ecto.gen_migration(igniter, repo, "add_error_tracker",
        body: migration_body,
        on_exists: :skip
      )
    end

    defp set_up_web_ui(igniter, app_name, router) do
      if router do
        Igniter.Project.Module.find_and_update_module!(igniter, router, fn zipper ->
          zipper =
            Igniter.Code.Common.add_code(
              zipper,
              """
              if Application.compile_env(#{inspect(app_name)}, :dev_routes) do
                use ErrorTracker.Web, :router

                scope "/dev" do
                  pipe_through :browser

                  error_tracker_dashboard "/errors"
                end
              end
              """,
              placement: :after
            )

          {:ok, zipper}
        end)
      else
        Igniter.add_warning(igniter, """
        No Phoenix router found or selected. Please ensure that Phoenix is set up
        and then run this installer again with

            mix igniter.install error_tracker
        """)
      end
    end
  end
else
  defmodule Mix.Tasks.ErrorTracker.Install do
    @shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use"

    @moduledoc __MODULE__.Docs.long_doc()

    use Mix.Task

    def run(_argv) do
      Mix.shell().error("""
      The task 'error_tracker.install' requires igniter. Please install igniter and try again.

      For more information, see: https://hexdocs.pm/igniter/readme.html#installation
      """)

      exit({:shutdown, 1})
    end
  end
end


================================================
FILE: mix.exs
================================================
defmodule ErrorTracker.MixProject do
  use Mix.Project

  def project do
    [
      app: :error_tracker,
      version: "0.8.0",
      elixir: "~> 1.15",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      package: package(),
      description: description(),
      source_url: "https://github.com/elixir-error-tracker/error-tracker",
      aliases: aliases(),
      name: "ErrorTracker",
      docs: [
        main: "ErrorTracker",
        formatters: ["html"],
        groups_for_modules: groups_for_modules(),
        extra_section: "GUIDES",
        extras: [
          "guides/Getting Started.md"
        ],
        api_reference: false,
        main: "getting-started"
      ]
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      mod: {ErrorTracker.Application, []},
      extra_applications: [:logger]
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_env), do: ["lib"]

  def package do
    [
      licenses: ["Apache-2.0"],
      links: %{
        "GitHub" => "https://github.com/elixir-error-tracker/error-tracker"
      },
      maintainers: [
        "Óscar de Arriba González",
        "Cristian Álvarez Belaustegui",
        "Víctor Ortiz Heredia"
      ],
      files: ~w(lib priv/static LICENSE mix.exs README.md .formatter.exs)
    ]
  end

  def description do
    "An Elixir-based built-in error tracking solution"
  end

  defp groups_for_modules do
    [
      Integrations: [
        ErrorTracker.Integrations.Oban,
        ErrorTracker.Integrations.Phoenix,
        ErrorTracker.Integrations.Plug
      ],
      Plugins: [
        ErrorTracker.Plugins.Pruner
      ],
      Schemas: [
        ErrorTracker.Error,
        ErrorTracker.Occurrence,
        ErrorTracker.Stacktrace,
        ErrorTracker.Stacktrace.Line
      ],
      "Web UI": [
        ErrorTracker.Web,
        ErrorTracker.Web.Router
      ]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:ecto_sql, "~> 3.13"},
      {:ecto, "~> 3.13"},
      {:phoenix_ecto, "~> 4.6"},
      {:phoenix_live_view, "~> 1.0"},
      {:plug, "~> 1.10"},
      # Dev dependencies
      {:bun, "~> 1.3", only: :dev},
      {:ex_doc, "~> 0.33", only: :dev},
      {:phoenix_live_reload, ">= 0.0.0", only: :dev},
      {:plug_cowboy, ">= 0.0.0", only: :dev},
      {:styler, "~> 1.11", only: [:dev, :test], runtime: false},
      {:tailwind, "~> 0.2", only: :dev},
      # Optional dependencies
      {:ecto_sqlite3, ">= 0.0.0", optional: true},
      {:igniter, "~> 0.5", optional: true},
      {:jason, "~> 1.1", optional: true},
      {:myxql, ">= 0.0.0", optional: true},
      {:postgrex, ">= 0.0.0", optional: true}
    ]
  end

  defp aliases do
    [
      dev: "run --no-halt dev.exs",
      "assets.install": ["bun.install", "cmd _build/bun install --cwd assets/"],
      "assets.watch": ["tailwind default --watch"],
      "assets.build": ["bun default", "tailwind default"]
    ]
  end
end


================================================
FILE: priv/repo/migrations/20240527155639_create_error_tracker_tables.exs
================================================
defmodule ErrorTracker.Repo.Migrations.CreateErrorTrackerTables do
  use Ecto.Migration

  defdelegate up, to: ErrorTracker.Migration
  defdelegate down, to: ErrorTracker.Migration
end


================================================
FILE: priv/repo/seeds.exs
================================================
adapter =
  case Application.get_env(:error_tracker, :ecto_adapter) do
    :postgres -> Ecto.Adapters.Postgres
    :sqlite3 -> Ecto.Adapters.SQLite3
  end

defmodule ErrorTrackerDev.Repo do
  use Ecto.Repo, otp_app: :error_tracker, adapter: adapter
end

ErrorTrackerDev.Repo.start_link()

ErrorTrackerDev.Repo.delete_all(ErrorTracker.Error)

errors =
  for i <- 1..100 do
    %{
      kind: "Error #{i}",
      reason: "Reason #{i}",
      source_line: "line",
      source_function: "function",
      status: :unresolved,
      fingerprint: "#{i}",
      last_occurrence_at: DateTime.utc_now(),
      inserted_at: DateTime.utc_now(),
      updated_at: DateTime.utc_now()
    }
  end

{_, errors} = dbg(ErrorTrackerDev.Repo.insert_all(ErrorTracker.Error, errors, returning: [:id]))

for error <- errors do
  occurrences =
    for _i <- 1..200 do
      %{
        context: %{},
        reason: "REASON",
        stacktrace: %ErrorTracker.Stacktrace{},
        error_id: error.id,
        inserted_at: DateTime.utc_now()
      }
    end

  ErrorTrackerDev.Repo.insert_all(ErrorTracker.Occurrence, occurrences)
end


================================================
FILE: priv/static/app.css
================================================
/*
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
*/

/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/

*,
::before,
::after {
  box-sizing: border-box;
  /* 1 */
  border-width: 0;
  /* 2 */
  border-style: solid;
  /* 2 */
  border-color: #e5e7eb;
  /* 2 */
}

::before,
::after {
  --tw-content: '';
}

/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/

html,
:host {
  line-height: 1.5;
  /* 1 */
  -webkit-text-size-adjust: 100%;
  /* 2 */
  -moz-tab-size: 4;
  /* 3 */
  -o-tab-size: 4;
     tab-size: 4;
  /* 3 */
  font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  /* 4 */
  font-feature-settings: normal;
  /* 5 */
  font-variation-settings: normal;
  /* 6 */
  -webkit-tap-highlight-color: transparent;
  /* 7 */
}

/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/

body {
  margin: 0;
  /* 1 */
  line-height: inherit;
  /* 2 */
}

/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/

hr {
  height: 0;
  /* 1 */
  color: inherit;
  /* 2 */
  border-top-width: 1px;
  /* 3 */
}

/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/

abbr:where([title]) {
  -webkit-text-decoration: underline dotted;
          text-decoration: underline dotted;
}

/*
Remove the default font size and weight for headings.
*/

h1,
h2,
h3,
h4,
h5,
h6 {
  font-size: inherit;
  font-weight: inherit;
}

/*
Reset links to optimize for opt-in styling instead of opt-out.
*/

a {
  color: inherit;
  text-decoration: inherit;
}

/*
Add the correct font weight in Edge and Safari.
*/

b,
strong {
  font-weight: bolder;
}

/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/

code,
kbd,
samp,
pre {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  /* 1 */
  font-feature-settings: normal;
  /* 2 */
  font-variation-settings: normal;
  /* 3 */
  font-size: 1em;
  /* 4 */
}

/*
Add the correct font size in all browsers.
*/

small {
  font-size: 80%;
}

/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/

table {
  text-indent: 0;
  /* 1 */
  border-color: inherit;
  /* 2 */
  border-collapse: collapse;
  /* 3 */
}

/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/

button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  /* 1 */
  font-feature-settings: inherit;
  /* 1 */
  font-variation-settings: inherit;
  /* 1 */
  font-size: 100%;
  /* 1 */
  font-weight: inherit;
  /* 1 */
  line-height: inherit;
  /* 1 */
  letter-spacing: inherit;
  /* 1 */
  color: inherit;
  /* 1 */
  margin: 0;
  /* 2 */
  padding: 0;
  /* 3 */
}

/*
Remove the inheritance of text transform in Edge and Firefox.
*/

button,
select {
  text-transform: none;
}

/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/

button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
  -webkit-appearance: button;
  /* 1 */
  background-color: transparent;
  /* 2 */
  background-image: none;
  /* 2 */
}

/*
Use the modern Firefox focus style for all focusable elements.
*/

:-moz-focusring {
  outline: auto;
}

/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/

:-moz-ui-invalid {
  box-shadow: none;
}

/*
Add the correct vertical alignment in Chrome and Firefox.
*/

progress {
  vertical-align: baseline;
}

/*
Correct the cursor style of increment and decrement buttons in Safari.
*/

::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
  height: auto;
}

/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/

[type='search'] {
  -webkit-appearance: textfield;
  /* 1 */
  outline-offset: -2px;
  /* 2 */
}

/*
Remove the inner padding in Chrome and Safari on macOS.
*/

::-webkit-search-decoration {
  -webkit-appearance: none;
}

/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/

::-webkit-file-upload-button {
  -webkit-appearance: button;
  /* 1 */
  font: inherit;
  /* 2 */
}

/*
Add the correct display in Chrome and Safari.
*/

summary {
  display: list-item;
}

/*
Removes the default spacing and border for appropriate elements.
*/

blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
  margin: 0;
}

fieldset {
  margin: 0;
  padding: 0;
}

legend {
  padding: 0;
}

ol,
ul,
menu {
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
Reset default styling for dialogs.
*/

dialog {
  padding: 0;
}

/*
Prevent resizing textareas horizontally by default.
*/

textarea {
  resize: vertical;
}

/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/

input::-moz-placeholder, textarea::-moz-placeholder {
  opacity: 1;
  /* 1 */
  color: #9ca3af;
  /* 2 */
}

input::placeholder,
textarea::placeholder {
  opacity: 1;
  /* 1 */
  color: #9ca3af;
  /* 2 */
}

/*
Set the default cursor for buttons.
*/

button,
[role="button"] {
  cursor: pointer;
}

/*
Make sure disabled buttons don't get the pointer cursor.
*/

:disabled {
  cursor: default;
}

/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
   This can trigger a poorly considered lint error in some tools but is included by design.
*/

img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
  display: block;
  /* 1 */
  vertical-align: middle;
  /* 2 */
}

/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/

img,
video {
  max-width: 100%;
  height: auto;
}

/* Make elements with the HTML hidden attribute stay hidden by default */

[hidden] {
  display: none;
}

[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
  background-color: #fff;
  border-color: #6b7280;
  border-width: 1px;
  border-radius: 0px;
  padding-top: 0.5rem;
  padding-right: 0.75rem;
  padding-bottom: 0.5rem;
  padding-left: 0.75rem;
  font-size: 1rem;
  line-height: 1.5rem;
  --tw-shadow: 0 0 #0000;
}

[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
  outline: 2px solid transparent;
  outline-offset: 2px;
  --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: #2563eb;
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
  border-color: #2563eb;
}

input::-moz-placeholder, textarea::-moz-placeholder {
  color: #6b7280;
  opacity: 1;
}

input::placeholder,textarea::placeholder {
  color: #6b7280;
  opacity: 1;
}

::-webkit-datetime-edit-fields-wrapper {
  padding: 0;
}

::-webkit-date-and-time-value {
  min-height: 1.5em;
}

::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
  padding-top: 0;
  padding-bottom: 0;
}

select {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
  background-position: right 0.5rem center;
  background-repeat: no-repeat;
  background-size: 1.5em 1.5em;
  padding-right: 2.5rem;
  -webkit-print-color-adjust: exact;
          print-color-adjust: exact;
}

[multiple] {
  background-image: initial;
  background-position: initial;
  background-repeat: unset;
  background-size: initial;
  padding-right: 0.75rem;
  -webkit-print-color-adjust: unset;
          print-color-adjust: unset;
}

[type='checkbox'],[type='radio'] {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
  padding: 0;
  -webkit-print-color-adjust: exact;
          print-color-adjust: exact;
  display: inline-block;
  vertical-align: middle;
  background-origin: border-box;
  -webkit-user-select: none;
     -moz-user-select: none;
          user-select: none;
  flex-shrink: 0;
  height: 1rem;
  width: 1rem;
  color: #2563eb;
  background-color: #fff;
  border-color: #6b7280;
  border-width: 1px;
  --tw-shadow: 0 0 #0000;
}

[type='checkbox'] {
  border-radius: 0px;
}

[type='radio'] {
  border-radius: 100%;
}

[type='checkbox']:focus,[type='radio']:focus {
  outline: 2px solid transparent;
  outline-offset: 2px;
  --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
  --tw-ring-offset-width: 2px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: #2563eb;
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}

[type='checkbox']:checked,[type='radio']:checked {
  border-color: transparent;
  background-color: currentColor;
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}

[type='checkbox']:checked {
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}

[type='radio']:checked {
  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
}

[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
  border-color: transparent;
  background-color: currentColor;
}

[type='checkbox']:indeterminate {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
  border-color: transparent;
  background-color: currentColor;
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}

[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
  border-color: transparent;
  background-color: currentColor;
}

[type='file'] {
  background: unset;
  border-color: inherit;
  border-width: 0;
  border-radius: 0;
  padding: 0;
  font-size: unset;
  line-height: inherit;
}

[type='file']:focus {
  outline: 1px solid ButtonText;
  outline: 1px auto -webkit-focus-ring-color;
}

*, ::before, ::after {
  --tw-border-spacing-x: 0;
  --tw-border-spacing-y: 0;
  --tw-translate-x: 0;
  --tw-translate-y: 0;
  --tw-rotate: 0;
  --tw-skew-x: 0;
  --tw-skew-y: 0;
  --tw-scale-x: 1;
  --tw-scale-y: 1;
  --tw-pan-x:  ;
  --tw-pan-y:  ;
  --tw-pinch-zoom:  ;
  --tw-scroll-snap-strictness: proximity;
  --tw-gradient-from-position:  ;
  --tw-gradient-via-position:  ;
  --tw-gradient-to-position:  ;
  --tw-ordinal:  ;
  --tw-slashed-zero:  ;
  --tw-numeric-figure:  ;
  --tw-numeric-spacing:  ;
  --tw-numeric-fraction:  ;
  --tw-ring-inset:  ;
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgb(59 130 246 / 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000;
  --tw-shadow: 0 0 #0000;
  --tw-shadow-colored: 0 0 #0000;
  --tw-blur:  ;
  --tw-brightness:  ;
  --tw-contrast:  ;
  --tw-grayscale:  ;
  --tw-hue-rotate:  ;
  --tw-invert:  ;
  --tw-saturate:  ;
  --tw-sepia:  ;
  --tw-drop-shadow:  ;
  --tw-backdrop-blur:  ;
  --tw-backdrop-brightness:  ;
  --tw-backdrop-contrast:  ;
  --tw-backdrop-grayscale:  ;
  --tw-backdrop-hue-rotate:  ;
  --tw-backdrop-invert:  ;
  --tw-backdrop-opacity:  ;
  --tw-backdrop-saturate:  ;
  --tw-backdrop-sepia:  ;
  --tw-contain-size:  ;
  --tw-contain-layout:  ;
  --tw-contain-paint:  ;
  --tw-contain-style:  ;
}

::backdrop {
  --tw-border-spacing-x: 0;
  --tw-border-spacing-y: 0;
  --tw-translate-x: 0;
  --tw-translate-y: 0;
  --tw-rotate: 0;
  --tw-skew-x: 0;
  --tw-skew-y: 0;
  --tw-scale-x: 1;
  --tw-scale-y: 1;
  --tw-pan-x:  ;
  --tw-pan-y:  ;
  --tw-pinch-zoom:  ;
  --tw-scroll-snap-strictness: proximity;
  --tw-gradient-from-position:  ;
  --tw-gradient-via-position:  ;
  --tw-gradient-to-position:  ;
  --tw-ordinal:  ;
  --tw-slashed-zero:  ;
  --tw-numeric-figure:  ;
  --tw-numeric-spacing:  ;
  --tw-numeric-fraction:  ;
  --tw-ring-inset:  ;
  --tw-ring-offset-width: 0px;
  --tw-ring-offset-color: #fff;
  --tw-ring-color: rgb(59 130 246 / 0.5);
  --tw-ring-offset-shadow: 0 0 #0000;
  --tw-ring-shadow: 0 0 #0000;
  --tw-shadow: 0 0 #0000;
  --tw-shadow-colored: 0 0 #0000;
  --tw-blur:  ;
  --tw-brightness:  ;
  --tw-contrast:  ;
  --tw-grayscale:  ;
  --tw-hue-rotate:  ;
  --tw-invert:  ;
  --tw-saturate:  ;
  --tw-sepia:  ;
  --tw-drop-shadow:  ;
  --tw-backdrop-blur:  ;
  --tw-backdrop-brightness:  ;
  --tw-backdrop-contrast:  ;
  --tw-backdrop-grayscale:  ;
  --tw-backdrop-hue-rotate:  ;
  --tw-backdrop-invert:  ;
  --tw-backdrop-opacity:  ;
  --tw-backdrop-saturate:  ;
  --tw-backdrop-sepia:  ;
  --tw-contain-size:  ;
  --tw-contain-layout:  ;
  --tw-contain-paint:  ;
  --tw-contain-style:  ;
}

.container {
  width: 100%;
}

@media (min-width: 640px) {
  .container {
    max-width: 640px;
  }
}

@media (min-width: 768px) {
  .container {
    max-width: 768px;
  }
}

@media (min-width: 1024px) {
  .container {
    max-width: 1024px;
  }
}

@media (min-width: 1280px) {
  .container {
    max-width: 1280px;
  }
}

@media (min-width: 1536px) {
  .container {
    max-width: 1536px;
  }
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

.static {
  position: static;
}

.absolute {
  position: absolute;
}

.relative {
  position: relative;
}

.inset-1 {
  inset: 0.25rem;
}

.mx-auto {
  margin-left: auto;
  margin-right: auto;
}

.my-1 {
  margin-top: 0.25rem;
  margin-bottom: 0.25rem;
}

.my-6 {
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
}

.mb-1 {
  margin-bottom: 0.25rem;
}

.mb-2 {
  margin-bottom: 0.5rem;
}

.mb-3 {
  margin-bottom: 0.75rem;
}

.mb-4 {
  margin-bottom: 1rem;
}

.me-2 {
  margin-inline-end: 0.5rem;
}

.ml-2 {
  margin-left: 0.5rem;
}

.mr-1 {
  margin-right: 0.25rem;
}

.mr-2 {
  margin-right: 0.5rem;
}

.mt-1 {
  margin-top: 0.25rem;
}

.mt-10 {
  margin-top: 2.5rem;
}

.mt-2 {
  margin-top: 0.5rem;
}

.mt-4 {
  margin-top: 1rem;
}

.mt-6 {
  margin-top: 1.5rem;
}

.block {
  display: block;
}

.inline-block {
  display: inline-block;
}

.inline {
  display: inline;
}

.flex {
  display: flex;
}

.inline-flex {
  display: inline-flex;
}

.table {
  display: table;
}

.grid {
  display: grid;
}

.hidden {
  display: none;
}

.\!h-4 {
  height: 1rem !important;
}

.h-10 {
  height: 2.5rem;
}

.h-5 {
  height: 1.25rem;
}

.\!w-4 {
  width: 1rem !important;
}

.w-10 {
  width: 2.5rem;
}

.w-11 {
  width: 2.75rem;
}

.w-28 {
  width: 7rem;
}

.w-5 {
  width: 1.25rem;
}

.w-72 {
  width: 18rem;
}

.w-full {
  width: 100%;
}

.table-fixed {
  table-layout: fixed;
}

.grid-cols-1 {
  grid-template-columns: repeat(1, minmax(0, 1fr));
}

.grid-cols-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.flex-col {
  flex-direction: column;
}

.flex-wrap {
  flex-wrap: wrap;
}

.items-center {
  align-items: center;
}

.justify-end {
  justify-content: flex-end;
}

.justify-center {
  justify-content: center;
}

.justify-between {
  justify-content: space-between;
}

.gap-2 {
  gap: 0.5rem;
}

.gap-y-4 {
  row-gap: 1rem;
}

.space-y-8 > :not([hidden]) ~ :not([hidden]) {
  --tw-space-y-reverse: 0;
  margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
  margin-bottom: calc(2rem * var(--tw-space-y-reverse));
}

.self-center {
  align-self: center;
}

.overflow-auto {
  overflow: auto;
}

.overflow-hidden {
  overflow: hidden;
}

.overflow-x-auto {
  overflow-x: auto;
}

.text-ellipsis {
  text-overflow: ellipsis;
}

.whitespace-nowrap {
  white-space: nowrap;
}

.rounded {
  border-radius: 0.25rem;
}

.rounded-lg {
  border-radius: 0.5rem;
}

.border {
  border-width: 1px;
}

.border-y {
  border-top-width: 1px;
  border-bottom-width: 1px;
}

.border-b {
  border-bottom-width: 1px;
}

.border-gray-400 {
  --tw-border-opacity: 1;
  border-color: rgb(156 163 175 / var(--tw-border-opacity));
}

.border-gray-600 {
  --tw-border-opacity: 1;
  border-color: rgb(75 85 99 / var(--tw-border-opacity));
}

.border-gray-900 {
  --tw-border-opacity: 1;
  border-color: rgb(17 24 39 / var(--tw-border-opacity));
}

.bg-blue-900 {
  --tw-bg-opacity: 1;
  background-color: rgb(30 58 138 / var(--tw-bg-opacity));
}

.bg-emerald-400\/10 {
  background-color: rgb(52 211 153 / 0.1);
}

.bg-gray-300\/10 {
  background-color: rgb(209 213 219 / 0.1);
}

.bg-gray-400\/10 {
  background-color: rgb(156 163 175 / 0.1);
}

.bg-gray-700 {
  --tw-bg-opacity: 1;
  background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}

.bg-gray-800 {
  --tw-bg-opacity: 1;
  background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}

.bg-gray-900 {
  --tw-bg-opacity: 1;
  background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}

.bg-indigo-900 {
  --tw-bg-opacity: 1;
  background-color: rgb(49 46 129 / var(--tw-bg-opacity));
}

.bg-pink-900 {
  --tw-bg-opacity: 1;
  background-color: rgb(131 24 67 / var(--tw-bg-opacity));
}

.bg-purple-900 {
  --tw-bg-opacity: 1;
  background-color: rgb(88 28 135 / var(--tw-bg-opacity));
}

.bg-red-400\/10 {
  background-color: rgb(248 113 113 / 0.1);
}

.bg-sky-500 {
  --tw-bg-opacity: 1;
  background-color: rgb(14 165 233 / var(--tw-bg-opacity));
}

.bg-yellow-900 {
  --tw-bg-opacity: 1;
  background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}

.p-2 {
  padding: 0.5rem;
}

.p-2\.5 {
  padding: 0.625rem;
}

.p-4 {
  padding: 1rem;
}

.px-2 {
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}

.px-3 {
  padding-left: 0.75rem;
  padding-right: 0.75rem;
}

.px-4 {
  padding-left: 1rem;
  padding-right: 1rem;
}

.py-1 {
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}

.py-2 {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

.py-3 {
  padding-top: 0.75rem;
  padding-bottom: 0.75rem;
}

.py-4 {
  padding-top: 1rem;
  padding-bottom: 1rem;
}

.py-8 {
  padding-top: 2rem;
  padding-bottom: 2rem;
}

.py-\[11\.5px\] {
  padding-top: 11.5px;
  padding-bottom: 11.5px;
}

.pl-2 {
  padding-left: 0.5rem;
}

.pr-2 {
  padding-right: 0.5rem;
}

.pr-5 {
  padding-right: 1.25rem;
}

.text-left {
  text-align: left;
}

.text-center {
  text-align: center;
}

.text-right {
  text-align: right;
}

.align-top {
  vertical-align: top;
}

.align-text-top {
  vertical-align: text-top;
}

.text-2xl {
  font-size: 1.5rem;
  line-height: 2rem;
}

.text-base {
  font-size: 1rem;
  line-height: 1.5rem;
}

.text-sm {
  font-size: 0.875rem;
  line-height: 1.25rem;
}

.text-xs {
  font-size: 0.75rem;
  line-height: 1rem;
}

.font-extralight {
  font-weight: 200;
}

.font-medium {
  font-weight: 500;
}

.font-normal {
  font-weight: 400;
}

.font-semibold {
  font-weight: 600;
}

.uppercase {
  text-transform: uppercase;
}

.text-black {
  --tw-text-opacity: 1;
  color: rgb(0 0 0 / var(--tw-text-opacity));
}

.text-blue-300 {
  --tw-text-opacity: 1;
  color: rgb(147 197 253 / var(--tw-text-opacity));
}

.text-emerald-300 {
  --tw-text-opacity: 1;
  color: rgb(110 231 183 / var(--tw-text-opacity));
}

.text-gray-300 {
  --tw-text-opacity: 1;
  color: rgb(209 213 219 / var(--tw-text-opacity));
}

.text-gray-400 {
  --tw-text-opacity: 1;
  color: rgb(156 163 175 / var(--tw-text-opacity));
}

.text-indigo-300 {
  --tw-text-opacity: 1;
  color: rgb(165 180 252 / var(--tw-text-opacity));
}

.text-pink-300 {
  --tw-text-opacity: 1;
  color: rgb(249 168 212 / var(--tw-text-opacity));
}

.text-purple-300 {
  --tw-text-opacity: 1;
  color: rgb(216 180 254 / var(--tw-text-opacity));
}

.text-red-300 {
  --tw-text-opacity: 1;
  color: rgb(252 165 165 / var(--tw-text-opacity));
}

.text-sky-500 {
  --tw-text-opacity: 1;
  color: rgb(14 165 233 / var(--tw-text-opacity));
}

.text-sky-600 {
  --tw-text-opacity: 1;
  color: rgb(2 132 199 / var(--tw-text-opacity));
}

.text-white {
  --tw-text-opacity: 1;
  color: rgb(255 255 255 / var(--tw-text-opacity));
}

.text-yellow-300 {
  --tw-text-opacity: 1;
  color: rgb(253 224 71 / var(--tw-text-opacity));
}

.placeholder-gray-400::-moz-placeholder {
  --tw-placeholder-opacity: 1;
  color: rgb(156 163 175 / var(--tw-placeholder-opacity));
}

.placeholder-gray-400::placeholder {
  --tw-placeholder-opacity: 1;
  color: rgb(156 163 175 / var(--tw-placeholder-opacity));
}

.shadow-md {
  --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}

.ring-1 {
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}

.ring-inset {
  --tw-ring-inset: inset;
}

.ring-emerald-400\/20 {
  --tw-ring-color: rgb(52 211 153 / 0.2);
}

.ring-gray-900 {
  --tw-ring-opacity: 1;
  --tw-ring-color: rgb(17 24 39 / var(--tw-ring-opacity));
}

.ring-red-400\/20 {
  --tw-ring-color: rgb(248 113 113 / 0.2);
}

.ring-offset-gray-800 {
  --tw-ring-offset-color: #1f2937;
}

.filter {
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}

::-webkit-scrollbar {
  height: 6px;
  width: 6px;
  --tw-bg-opacity: 1;
  background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}

::-webkit-scrollbar-thumb {
  --tw-bg-opacity: 1;
  background-color: rgb(107 114 128 / var(--tw-bg-opacity));
  border-radius: 4px;
}

.last\:border-b-0:last-child {
  border-bottom-width: 0px;
}

.last-of-type\:border-b-0:last-of-type {
  border-bottom-width: 0px;
}

.hover\:bg-gray-700:hover {
  --tw-bg-opacity: 1;
  background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}

.hover\:bg-gray-800:hover {
  --tw-bg-opacity: 1;
  background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}

.hover\:bg-gray-800\/60:hover {
  background-color: rgb(31 41 55 / 0.6);
}

.hover\:bg-sky-700:hover {
  --tw-bg-opacity: 1;
  background-color: rgb(3 105 161 / var(--tw-bg-opacity));
}

.hover\:text-white:hover {
  --tw-text-opacity: 1;
  color: rgb(255 255 255 / var(--tw-text-opacity));
}

.hover\:text-white\/80:hover {
  color: rgb(255 255 255 / 0.8);
}

.focus\:border-blue-500:focus {
  --tw-border-opacity: 1;
  border-color: rgb(59 130 246 / var(--tw-border-opacity));
}

.focus\:outline-none:focus {
  outline: 2px solid transparent;
  outline-offset: 2px;
}

.focus\:ring-2:focus {
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}

.focus\:ring-blue-500:focus {
  --tw-ring-opacity: 1;
  --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}

.focus\:ring-gray-500:focus {
  --tw-ring-opacity: 1;
  --tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));
}

.focus\:ring-sky-600:focus {
  --tw-ring-opacity: 1;
  --tw-ring-color: rgb(2 132 199 / var(--tw-ring-opacity));
}

.active\:text-white\/80:active {
  color: rgb(255 255 255 / 0.8);
}

.phx-submit-loading\:opacity-75.phx-submit-loading {
  opacity: 0.75;
}

.phx-submit-loading .phx-submit-loading\:opacity-75 {
  opacity: 0.75;
}

@media (min-width: 640px) {
  .sm\:rounded-lg {
    border-radius: 0.5rem;
  }
}

@media (min-width: 768px) {
  .md\:col-span-3 {
    grid-column: span 3 / span 3;
  }

  .md\:mt-0 {
    margin-top: 0px;
  }

  .md\:block {
    display: block;
  }

  .md\:hidden {
    display: none;
  }

  .md\:w-auto {
    width: auto;
  }

  .md\:grid-cols-4 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .md\:flex-row {
    flex-direction: row;
  }

  .md\:space-x-3 > :not([hidden]) ~ :not([hidden]) {
    --tw-space-x-reverse: 0;
    margin-right: calc(0.75rem * var(--tw-space-x-reverse));
    margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
  }

  .md\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
    --tw-space-x-reverse: 0;
    margin-right: calc(2rem * var(--tw-space-x-reverse));
    margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
  }

  .md\:border-0 {
    border-width: 0px;
  }

  .md\:border-r {
    border-right-width: 1px;
  }

  .md\:border-gray-600 {
    --tw-border-opacity: 1;
    border-color: rgb(75 85 99 / var(--tw-border-opacity));
  }

  .md\:bg-gray-800 {
    --tw-bg-opacity: 1;
    background-color: rgb(31 41 55 / var(--tw-bg-opacity));
  }

  .md\:p-0 {
    padding: 0px;
  }

  .md\:pl-0 {
    padding-left: 0px;
  }

  .md\:hover\:bg-transparent:hover {
    background-color: transparent;
  }

  .md\:hover\:text-sky-500:hover {
    --tw-text-opacity: 1;
    color: rgb(14 165 233 / var(--tw-text-opacity));
  }
}

.rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) {
  --tw-space-x-reverse: 1;
}

.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
  text-align: right;
}


================================================
FILE: priv/static/app.js
================================================
var C=Object.create;var{defineProperty:b,getPrototypeOf:x,getOwnPropertyNames:E}=Object;var F=Object.prototype.hasOwnProperty;var I=(n,s,u)=>{u=n!=null?C(x(n)):{};const t=s||!n||!n.__esModule?b(u,"default",{value:n,enumerable:!0}):u;for(let o of E(n))if(!F.call(t,o))b(t,o,{get:()=>n[o],enumerable:!0});return t};var w=(n,s)=>()=>(s||n((s={exports:{}}).exports,s),s.exports);var y=w((m,g)=>{(function(n,s){function u(){t.width=n.innerWidth,t.height=5*i.barThickness;var e=t.getContext("2d");e.shadowBlur=i.shadowBlur,e.shadowColor=i.shadowColor;var r,a=e.createLinearGradient(0,0,t.width,0);for(r in i.barColors)a.addColorStop(r,i.barColors[r]);e.lineWidth=i.barThickness,e.beginPath(),e.moveTo(0,i.barThickness/2),e.lineTo(Math.ceil(o*t.width),i.barThickness/2),e.strokeStyle=a,e.stroke()}var t,o,c,d=null,p=null,h=null,i={autoRun:!0,barThickness:3,barColors:{0:"rgba(26,  188, 156, .9)",".25":"rgba(52,  152, 219, .9)",".50":"rgba(241, 196, 15,  .9)",".75":"rgba(230, 126, 34,  .9)","1.0":"rgba(211, 84,  0,   .9)"},shadowBlur:10,shadowColor:"rgba(0,   0,   0,   .6)",className:null},l={config:function(e){for(var r in e)i.hasOwnProperty(r)&&(i[r]=e[r])},show:function(e){var r,a;c||(e?h=h||setTimeout(()=>l.show(),e):(c=!0,p!==null&&n.cancelAnimationFrame(p),t||((a=(t=s.createElement("canvas")).style).position="fixed",a.top=a.left=a.right=a.margin=a.padding=0,a.zIndex=100001,a.display="none",i.className&&t.classList.add(i.className),r="resize",e=u,(a=n).addEventListener?a.addEventListener(r,e,!1):a.attachEvent?a.attachEvent("on"+r,e):a["on"+r]=e),t.parentElement||s.body.appendChild(t),t.style.opacity=1,t.style.display="block",l.progress(0),i.autoRun&&function v(){d=n.requestAnimationFrame(v),l.progress("+"+0.05*Math.pow(1-Math.sqrt(o),2))}()))},progress:function(e){return e===void 0||(typeof e=="string"&&(e=(0<=e.indexOf("+")||0<=e.indexOf("-")?o:0)+parseFloat(e)),o=1<e?1:e,u()),o},hide:function(){clearTimeout(h),h=null,c&&(c=!1,d!=null&&(n.cancelAnimationFrame(d),d=null),function e(){return 1<=l.progress("+.1")&&(t.style.opacity-=0.05,t.style.opacity<=0.05)?(t.style.display="none",void(p=null)):void(p=n.requestAnimationFrame(e))}())}};typeof g=="object"&&typeof m=="object"?g.exports=l:typeof define=="function"&&define.amd?define(function(){return l}):this.topbar=l}).call(m,window,document)});var f=I(y(),1),A=document.querySelector("meta[name='csrf-token']").getAttribute("content"),B=document.querySelector("meta[name='live-path']").getAttribute("content"),M=document.querySelector("meta[name='live-transport']").getAttribute("content"),N={JsonPrettyPrint:{mounted(){this.formatJson()},updated(){this.formatJson()},formatJson(){try{const n=this.el.textContent.trim(),s=JSON.stringify(JSON.parse(n),null,2);this.el.textContent=s}catch(n){console.error("Error formatting JSON:",n)}}}},T=new LiveView.LiveSocket(B,Phoenix.Socket,{transport:M==="longpoll"?Phoenix.LongPoll:WebSocket,params:{_csrf_token:A},hooks:N});f.default.config({barColors:{0:"#29d"},shadowColor:"rgba(0, 0, 0, .3)"});window.addEventListener("phx:page-loading-start",(n)=>f.default.show(300));window.addEventListener("phx:page-loading-stop",(n)=>f.default.hide());T.connect();window.liveSocket=T;


================================================
FILE: test/error_tracker/filter_test.exs
================================================
defmodule ErrorTracker.FilterTest do
  use ErrorTracker.Test.Case

  setup context do
    if filter = context[:filter] do
      previous_setting = Application.get_env(:error_tracker, :filter)
      Application.put_env(:error_tracker, :filter, filter)
      # Ensure that the application env is restored after each test
      on_exit(fn -> Application.put_env(:error_tracker, :filter, previous_setting) end)
    end

    []
  end

  @sensitive_ctx %{
    "request" => %{
      "headers" => %{
        "accept" => "application/json, text/plain, */*",
        "authorization" => "Bearer 12341234"
      }
    }
  }

  test "without an filter, context objects are saved as they are." do
    assert %ErrorTracker.Occurrence{context: ctx} =
             report_error(fn -> raise "BOOM" end, @sensitive_ctx)

    assert ctx == @sensitive_ctx
  end

  @tag filter: ErrorTracker.FilterTest.AuthHeaderHider
  test "user defined filter should be used to sanitize the context before it's saved." do
    assert %ErrorTracker.Occurrence{context: ctx} =
             report_error(fn -> raise "BOOM" end, @sensitive_ctx)

    assert ctx != @sensitive_ctx

    cleaned_header_value =
      ctx |> Map.get("request") |> Map.get("headers") |> Map.get("authorization")

    assert cleaned_header_value == "REMOVED"
  end
end

defmodule ErrorTracker.FilterTest.AuthHeaderHider do
  @moduledoc false
  @behaviour ErrorTracker.Filter

  def sanitize(context) do
    context
    |> Enum.map(fn
      {"authorization", _} ->
        {"authorization", "REMOVED"}

      o ->
        o
    end)
    |> Map.new(fn
      {key, val} when is_map(val) -> {key, sanitize(val)}
      o -> o
    end)
  end
end


================================================
FILE: test/error_tracker/ignorer_test.exs
================================================
defmodule ErrorTracker.IgnorerTest do
  use ErrorTracker.Test.Case

  setup context do
    if ignorer = context[:ignorer] do
      previous_setting = Application.get_env(:error_tracker, :ignorer)
      Application.put_env(:error_tracker, :ignorer, ignorer)
      # Ensure that the application env is restored after each test
      on_exit(fn -> Application.put_env(:error_tracker, :ignorer, previous_setting) end)
    end

    []
  end

  @tag ignorer: ErrorTracker.EveryErrorIgnorer
  test "with an ignorer ignores errors" do
    assert :noop = report_error(fn -> raise "[IGNORE] Sample error" end)
    assert %ErrorTracker.Occurrence{} = report_error(fn -> raise "Sample error" end)
  end

  @tag ignorer: false
  test "without an ignorer does not ignore errors" do
    assert %ErrorTracker.Occurrence{} = report_error(fn -> raise "[IGNORE] Sample error" end)
    assert %ErrorTracker.Occurrence{} = report_error(fn -> raise "Sample error" end)
  end
end

defmodule ErrorTracker.EveryErrorIgnorer do
  @moduledoc false
  @behaviour ErrorTracker.Ignorer

  @impl true
  def ignore?(error, _context) do
    String.contains?(error.reason, "[IGNORE]")
  end
end


================================================
FILE: test/error_tracker/schemas/occurrence_test.exs
================================================
defmodule ErrorTracker.OccurrenceTest do
  use ErrorTracker.Test.Case

  import Ecto.Changeset

  alias ErrorTracker.Occurrence
  alias ErrorTracker.Stacktrace

  describe inspect(&Occurrence.changeset/2) do
    test "works as expected with valid data" do
      attrs = %{context: %{foo: :bar}, reason: "Test reason", stacktrace: %Stacktrace{}}
      changeset = Occurrence.changeset(%Occurrence{}, attrs)

      assert changeset.valid?
    end

    test "validates required fields" do
      changeset = Occurrence.changeset(%Occurrence{}, %{})

      refute changeset.valid?
      assert {_, [validation: :required]} = changeset.errors[:reason]
      assert {_, [validation: :required]} = changeset.errors[:stacktrace]
    end

    @tag capture_log: true
    test "if context is not serializable, an error messgae is stored" do
      attrs = %{
        context: %{foo: %ErrorTracker.Error{}},
        reason: "Test reason",
        stacktrace: %Stacktrace{}
      }

      changeset = Occurrence.changeset(%Occurrence{}, attrs)

      assert %{error: err} = get_field(changeset, :context)
      assert err =~ "not serializable to JSON"
    end
  end
end


================================================
FILE: test/error_tracker/telemetry_test.exs
================================================
defmodule ErrorTracker.TelemetryTest do
  use ErrorTracker.Test.Case

  alias ErrorTracker.Error
  alias ErrorTracker.Occurrence

  setup do
    attach_telemetry()

    :ok
  end

  test "events are emitted for new errors" do
    {exception, stacktrace} =
      try do
        raise "This is a test"
      rescue
        e -> {e, __STACKTRACE__}
      end

    # Since the error is new, both the new error and new occurrence events will be emitted
    %Occurrence{error: error = %Error{}} = ErrorTracker.report(exception, stacktrace)
    assert_receive {:telemetry_event, [:error_tracker, :error, :new], _, %{error: %Error{}}}

    assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, %{occurrence: %Occurrence{}, muted: false}}

    # The error is already known so the new error event won't be emitted
    ErrorTracker.report(exception, stacktrace)

    refute_receive {:telemetry_event, [:error_tracker, :error, :new], _, %{error: %Error{}}},
                   150

    assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, %{occurrence: %Occurrence{}, muted: false}}

    # The error is muted so the new occurrence event will include the muted=true metadata
    ErrorTracker.mute(error)
    ErrorTracker.report(exception, stacktrace)

    assert_receive {:telemetry_event, [:error_tracker, :occurrence, :new], _, %{occurrence: %Occurrence{}, muted: true}}
  end

  test "events are emitted for resolved and unresolved errors" do
    %Occurrence{error: error = %Error{}} = report_error(fn -> raise "This is a test" end)

    # The resolved event will be emitted
    {:ok, resolved = %Error{}} = ErrorTracker.resolve(error)
    assert_receive {:telemetry_event, [:error_tracker, :error, :resolved], _, %{error: %Error{}}}

    # The unresolved event will be emitted
    {:ok, _unresolved} = ErrorTracker.unresolve(resolved)

    assert_receive {:telemetry_event, [:error_tracker, :error, :unresolved], _, %{error: %Error{}}}
  end
end


================================================
FILE: test/error_tracker_test.exs
================================================
defmodule ErrorTrackerTest do
  use ErrorTracker.Test.Case

  alias ErrorTracker.Error
  alias ErrorTracker.Occurrence

  # We use this file path because for some reason the test scripts are not
  # handled as part of the application, so the last line of the app executed is
  # on the case module.
  @relative_file_path "test/support/case.ex"

  describe inspect(&ErrorTracker.report/3) do
    setup context do
      if Map.has_key?(context, :enabled) do
        Application.put_env(:error_tracker, :enabled, context[:enabled])
        # Ensure that the application env is restored after each test
        on_exit(fn -> Application.delete_env(:error_tracker, :enabled) end)
      end

      []
    end

    test "reports exceptions" do
      %Occurrence{error: error = %Error{}} =
        report_error(fn -> raise "This is a test" end)

      assert error.kind == to_string(RuntimeError)
      assert error.reason == "This is a test"
      assert error.source_line =~ @relative_file_path
    end

    test "reports badarith errors" do
      string_var = to_string(1)

      %Occurrence{error: error = %Error{}, stacktrace: %{lines: [last_line | _]}} =
        report_error(fn -> 1 + string_var end)

      assert error.kind == to_string(ArithmeticError)
      assert error.reason == "bad argument in arithmetic expression"

      # Elixir 1.17.0 reports these errors differently than previous versions
      if Version.compare(System.version(), "1.17.0") == :lt do
        assert last_line.module == "ErrorTrackerTest"
        assert last_line.function =~ "&ErrorTracker.report/3 reports badarith errors"
        assert last_line.arity == 1
        assert last_line.file
        assert last_line.line
      else
        assert last_line.module == "erlang"
        assert last_line.function == "+"
        assert last_line.arity == 2
        refute last_line.file
        refute last_line.line
      end
    end

    test "reports undefined function errors" do
      # This function does not exist and will raise when called
      {m, f, a} = 
Download .txt
gitextract_9aka67fi/

├── .formatter.exs
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── elixir.yml
├── .gitignore
├── .tool-versions
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets/
│   ├── bun.lockb
│   ├── css/
│   │   └── app.css
│   ├── js/
│   │   └── app.js
│   ├── package.json
│   └── tailwind.config.js
├── config/
│   ├── config.exs
│   ├── dev.example.exs
│   └── test.example.exs
├── dev.exs
├── guides/
│   └── Getting Started.md
├── lib/
│   ├── error_tracker/
│   │   ├── application.ex
│   │   ├── filter.ex
│   │   ├── ignorer.ex
│   │   ├── integrations/
│   │   │   ├── oban.ex
│   │   │   ├── phoenix.ex
│   │   │   └── plug.ex
│   │   ├── migration/
│   │   │   ├── mysql/
│   │   │   │   ├── v03.ex
│   │   │   │   ├── v04.ex
│   │   │   │   └── v05.ex
│   │   │   ├── mysql.ex
│   │   │   ├── postgres/
│   │   │   │   ├── v01.ex
│   │   │   │   ├── v02.ex
│   │   │   │   ├── v03.ex
│   │   │   │   ├── v04.ex
│   │   │   │   └── v05.ex
│   │   │   ├── postgres.ex
│   │   │   ├── sql_migrator.ex
│   │   │   ├── sqlite/
│   │   │   │   ├── v02.ex
│   │   │   │   ├── v03.ex
│   │   │   │   ├── v04.ex
│   │   │   │   └── v05.ex
│   │   │   └── sqlite.ex
│   │   ├── migration.ex
│   │   ├── plugins/
│   │   │   └── pruner.ex
│   │   ├── repo.ex
│   │   ├── schemas/
│   │   │   ├── error.ex
│   │   │   ├── occurrence.ex
│   │   │   └── stacktrace.ex
│   │   ├── telemetry.ex
│   │   ├── web/
│   │   │   ├── components/
│   │   │   │   ├── core_components.ex
│   │   │   │   ├── layouts/
│   │   │   │   │   ├── live.html.heex
│   │   │   │   │   └── root.html.heex
│   │   │   │   └── layouts.ex
│   │   │   ├── helpers.ex
│   │   │   ├── hooks/
│   │   │   │   └── set_assigns.ex
│   │   │   ├── live/
│   │   │   │   ├── dashboard.ex
│   │   │   │   ├── dashboard.html.heex
│   │   │   │   ├── show.ex
│   │   │   │   └── show.html.heex
│   │   │   ├── router/
│   │   │   │   └── routes.ex
│   │   │   ├── router.ex
│   │   │   └── search.ex
│   │   └── web.ex
│   ├── error_tracker.ex
│   └── mix/
│       └── tasks/
│           └── error_tracker.install.ex
├── mix.exs
├── priv/
│   ├── repo/
│   │   ├── migrations/
│   │   │   └── 20240527155639_create_error_tracker_tables.exs
│   │   └── seeds.exs
│   └── static/
│       ├── app.css
│       └── app.js
└── test/
    ├── error_tracker/
    │   ├── filter_test.exs
    │   ├── ignorer_test.exs
    │   ├── schemas/
    │   │   └── occurrence_test.exs
    │   └── telemetry_test.exs
    ├── error_tracker_test.exs
    ├── integrations/
    │   └── plug_test.exs
    ├── support/
    │   ├── case.ex
    │   ├── lite_repo.ex
    │   ├── mysql_repo.ex
    │   └── repo.ex
    └── test_helper.exs
Download .txt
SYMBOL INDEX (274 symbols across 57 files)

FILE: assets/js/app.js
  method mounted (line 10) | mounted() {
  method updated (line 13) | updated() {
  method formatJson (line 16) | formatJson() {

FILE: dev.exs
  class ErrorTrackerDev.Repo (line 24) | defmodule ErrorTrackerDev.Repo
    method migrate (line 37) | def migrate do
  class Migration (line 29) | defmodule Migration
    method up (line 33) | def up, do: ErrorTracker.Migration.up()
    method down (line 34) | def down, do: ErrorTracker.Migration.down()
  class ErrorTrackerDev.Controller (line 42) | defmodule ErrorTrackerDev.Controller
    method index (line 49) | def index(conn, _params) do
    method index (line 53) | def index(assigns) do
    method noroute (line 63) | def noroute(conn, _params) do
    method exception (line 69) | def exception(_conn, _params) do
    method exit (line 77) | def exit(_conn, _params) do
  class ErrorTrackerDev.Live (line 84) | defmodule ErrorTrackerDev.Live
    method mount (line 88) | def mount(params, _session, socket) do
    method handle_params (line 96) | def handle_params(params, _uri, socket) do
    method handle_event (line 104) | def handle_event("crash_on_handle_event", _params, _socket) do
    method handle_event (line 108) | def handle_event("crash_on_render", _params, socket) do
    method handle_event (line 112) | def handle_event("genserver-timeout", _params, socket) do
    method render (line 117) | def render(assigns) do
  class ErrorTrackerDev.Router (line 173) | defmodule ErrorTrackerDev.Router
  class ErrorTrackerDev.Endpoint (line 199) | defmodule ErrorTrackerDev.Endpoint
    method set_csp (line 219) | defp set_csp(conn, _opts) do
  class ErrorTrackerDev.ErrorView (line 239) | defmodule ErrorTrackerDev.ErrorView
    method render (line 240) | def render("404.html", _assigns) do
    method render (line 244) | def render("500.html", _assigns) do
  class ErrorTrackerDev.GenServer (line 249) | defmodule ErrorTrackerDev.GenServer
    method start_link (line 255) | def start_link(_) do
    method init (line 262) | def init(initial_state) do
    method handle_call (line 267) | def handle_call(:timeout, _from, state) do
  class ErrorTrackerDev.Exception (line 273) | defmodule ErrorTrackerDev.Exception
  class ErrorTrackerDev.Telemetry (line 278) | defmodule ErrorTrackerDev.Telemetry
    method handle_event (line 280) | def handle_event(event, measure, metadata, _opts) do

FILE: lib/error_tracker.ex
  class ErrorTracker (line 1) | defmodule ErrorTracker
    method report (line 134) | def report(exception, stacktrace, given_context \\ %{}) do
    method resolve (line 157) | def resolve(%Error{status: :unresolved} = error) do
    method unresolve (line 170) | def unresolve(%Error{status: :resolved} = error) do
    method mute (line 191) | def mute(%Error{} = error) do
    method unmute (line 204) | def unmute(%Error{} = error) do
    method get_context (line 247) | def get_context do
    method get_breadcrumbs (line 281) | def get_breadcrumbs do
    method enabled? (line 285) | defp enabled? do
    method ignored? (line 289) | defp ignored?(error, context) do
    method sanitize_context (line 295) | defp sanitize_context(context) do
    method normalize_exception (line 307) | defp normalize_exception({kind, ex}, stacktrace) do
    method safe_to_string (line 314) | defp safe_to_string(term) do
    method exception_breadcrumbs (line 321) | defp exception_breadcrumbs(exception) do
    method upsert_error! (line 329) | defp upsert_error!(error, stacktrace, context, breadcrumbs, reason) do
    method __default_json_encoder__ (line 405) | def __default_json_encoder__, do: @default_json_encoder

FILE: lib/error_tracker/application.ex
  class ErrorTracker.Application (line 1) | defmodule ErrorTracker.Application
    method start (line 6) | def start(_type, _args) do
    method attach_handlers (line 14) | defp attach_handlers do

FILE: lib/error_tracker/filter.ex
  class ErrorTracker.Filter (line 1) | defmodule ErrorTracker.Filter

FILE: lib/error_tracker/ignorer.ex
  class ErrorTracker.Ignorer (line 1) | defmodule ErrorTracker.Ignorer

FILE: lib/error_tracker/integrations/oban.ex
  class ErrorTracker.Integrations.Oban (line 1) | defmodule ErrorTracker.Integrations.Oban
    method attach (line 54) | def attach do
    method handle_event (line 61) | def handle_event([:oban, :job, :start], _measurements, metadata, :no_c...
    method handle_event (line 74) | def handle_event([:oban, :job, :exception], _measurements, metadata, :...

FILE: lib/error_tracker/integrations/phoenix.ex
  class ErrorTracker.Integrations.Phoenix (line 1) | defmodule ErrorTracker.Integrations.Phoenix
    method attach (line 68) | def attach do
    method handle_event (line 75) | def handle_event([:phoenix, :router_dispatch, :start], _measurements, ...
    method handle_event (line 79) | def handle_event([:phoenix, :router_dispatch, :exception], _measuremen...
    method handle_event (line 92) | def handle_event([:phoenix, :live_view, :mount, :start], _, metadata, ...
    method handle_event (line 98) | def handle_event([:phoenix, :live_view, :handle_params, :start], _, me...
    method handle_event (line 105) | def handle_event([:phoenix, :live_view, :handle_event, :exception], _,...
    method handle_event (line 112) | def handle_event([:phoenix, :live_view, _action, :exception], _, metad...
    method handle_event (line 116) | def handle_event([:phoenix, :live_component, :update, :exception], _, ...
    method handle_event (line 122) | def handle_event([:phoenix, :live_component, :handle_event, :exception...

FILE: lib/error_tracker/integrations/plug.ex
  class ErrorTracker.Integrations.Plug (line 1) | defmodule ErrorTracker.Integrations.Plug
    method report_error (line 99) | def report_error(conn, reason, stack) do
    method set_context (line 110) | def set_context(%Plug.Conn{} = conn) do
    method build_context (line 116) | defp build_context(%Plug.Conn{} = conn) do
    method remote_ip (line 132) | defp remote_ip(%Plug.Conn{} = conn) do

FILE: lib/error_tracker/migration.ex
  class ErrorTracker.Migration (line 1) | defmodule ErrorTracker.Migration
    method migrator (line 114) | defp migrator do

FILE: lib/error_tracker/migration/mysql.ex
  class ErrorTracker.Migration.MySQL (line 1) | defmodule ErrorTracker.Migration.MySQL
    method up (line 14) | def up(opts) do
    method down (line 20) | def down(opts) do
    method current_version (line 26) | def current_version(opts) do
    method with_defaults (line 31) | defp with_defaults(opts, version) do

FILE: lib/error_tracker/migration/mysql/v03.ex
  class ErrorTracker.Migration.MySQL.V03 (line 1) | defmodule ErrorTracker.Migration.MySQL.V03
    method up (line 6) | def up(_opts) do
    method down (line 46) | def down(_opts) do

FILE: lib/error_tracker/migration/mysql/v04.ex
  class ErrorTracker.Migration.MySQL.V04 (line 1) | defmodule ErrorTracker.Migration.MySQL.V04
    method up (line 6) | def up(_opts) do
    method down (line 12) | def down(_opts) do

FILE: lib/error_tracker/migration/mysql/v05.ex
  class ErrorTracker.Migration.MySQL.V05 (line 1) | defmodule ErrorTracker.Migration.MySQL.V05
    method up (line 6) | def up(_opts) do
    method down (line 12) | def down(_opts) do

FILE: lib/error_tracker/migration/postgres.ex
  class ErrorTracker.Migration.Postgres (line 1) | defmodule ErrorTracker.Migration.Postgres
    method up (line 15) | def up(opts) do
    method down (line 21) | def down(opts) do
    method current_version (line 27) | def current_version(opts) do
    method with_defaults (line 32) | defp with_defaults(opts, version) do

FILE: lib/error_tracker/migration/postgres/v01.ex
  class ErrorTracker.Migration.Postgres.V01 (line 1) | defmodule ErrorTracker.Migration.Postgres.V01
    method up (line 8) | def up(%{create_schema: create_schema, prefix: prefix} = opts) do
    method down (line 67) | def down(%{prefix: prefix}) do
    method current_version_legacy (line 73) | def current_version_legacy(opts) do

FILE: lib/error_tracker/migration/postgres/v02.ex
  class ErrorTracker.Migration.Postgres.V02 (line 1) | defmodule ErrorTracker.Migration.Postgres.V02
    method up (line 6) | def up(%{prefix: prefix}) do
    method down (line 20) | def down(%{prefix: prefix}) do

FILE: lib/error_tracker/migration/postgres/v03.ex
  class ErrorTracker.Migration.Postgres.V03 (line 1) | defmodule ErrorTracker.Migration.Postgres.V03
    method up (line 6) | def up(%{prefix: prefix}) do
    method down (line 10) | def down(%{prefix: prefix}) do

FILE: lib/error_tracker/migration/postgres/v04.ex
  class ErrorTracker.Migration.Postgres.V04 (line 1) | defmodule ErrorTracker.Migration.Postgres.V04
    method up (line 6) | def up(%{prefix: prefix}) do
    method down (line 12) | def down(%{prefix: prefix}) do

FILE: lib/error_tracker/migration/postgres/v05.ex
  class ErrorTracker.Migration.Postgres.V05 (line 1) | defmodule ErrorTracker.Migration.Postgres.V05
    method up (line 6) | def up(%{prefix: prefix}) do
    method down (line 12) | def down(%{prefix: prefix}) do

FILE: lib/error_tracker/migration/sql_migrator.ex
  class ErrorTracker.Migration.SQLMigrator (line 1) | defmodule ErrorTracker.Migration.SQLMigrator
    method migrate_up (line 10) | def migrate_up(migrator, opts, initial_version) do
    method migrate_down (line 25) | def migrate_down(migrator, opts, initial_version) do
    method current_version (line 33) | def current_version(opts) do
    method change (line 49) | defp change(migrator, versions_range, direction, opts) do
    method record_version (line 63) | defp record_version(_opts, 0), do: :ok
    method record_version (line 65) | defp record_version(opts, version) do
    method meta_table_exists? (line 94) | defp meta_table_exists?(repo, opts) do

FILE: lib/error_tracker/migration/sqlite.ex
  class ErrorTracker.Migration.SQLite (line 1) | defmodule ErrorTracker.Migration.SQLite
    method up (line 14) | def up(opts) do
    method down (line 20) | def down(opts) do
    method current_version (line 26) | def current_version(opts) do
    method with_defaults (line 31) | defp with_defaults(opts, version) do

FILE: lib/error_tracker/migration/sqlite/v02.ex
  class ErrorTracker.Migration.SQLite.V02 (line 1) | defmodule ErrorTracker.Migration.SQLite.V02
    method up (line 6) | def up(_opts) do
    method down (line 44) | def down(_opts) do

FILE: lib/error_tracker/migration/sqlite/v03.ex
  class ErrorTracker.Migration.SQLite.V03 (line 1) | defmodule ErrorTracker.Migration.SQLite.V03
    method up (line 6) | def up(_opts) do
    method down (line 10) | def down(_opts) do

FILE: lib/error_tracker/migration/sqlite/v04.ex
  class ErrorTracker.Migration.SQLite.V04 (line 1) | defmodule ErrorTracker.Migration.SQLite.V04
    method up (line 6) | def up(_opts) do
    method down (line 12) | def down(_opts) do

FILE: lib/error_tracker/migration/sqlite/v05.ex
  class ErrorTracker.Migration.SQLite.V05 (line 1) | defmodule ErrorTracker.Migration.SQLite.V05
    method up (line 6) | def up(_opts) do
    method down (line 12) | def down(_opts) do

FILE: lib/error_tracker/plugins/pruner.ex
  class ErrorTracker.Plugins.Pruner (line 1) | defmodule ErrorTracker.Plugins.Pruner
    method prune_errors (line 74) | def prune_errors(opts \\ []) do
    method prune_occurrences (line 101) | defp prune_occurrences(occurrences_query) do
    method start_link (line 113) | def start_link(state \\ []) do
    method init (line 119) | def init(state \\ []) do
    method handle_info (line 131) | def handle_info(:prune, state) do
    method schedule_prune (line 137) | defp schedule_prune(%{interval: interval} = state) do

FILE: lib/error_tracker/repo.ex
  class ErrorTracker.Repo (line 1) | defmodule ErrorTracker.Repo
    method insert! (line 4) | def insert!(struct_or_changeset, opts \\ []) do
    method update (line 8) | def update(changeset, opts \\ []) do
    method get (line 12) | def get(queryable, id, opts \\ []) do
    method get! (line 16) | def get!(queryable, id, opts \\ []) do
    method one (line 20) | def one(queryable, opts \\ []) do
    method all (line 24) | def all(queryable, opts \\ []) do
    method delete_all (line 28) | def delete_all(queryable, opts \\ []) do
    method aggregate (line 32) | def aggregate(queryable, aggregate, opts \\ []) do
    method transaction (line 36) | def transaction(fun_or_multi, opts \\ []) do
    method with_adapter (line 40) | def with_adapter(fun) do
    method dispatch (line 51) | defp dispatch(action, args, opts) do
    method repo (line 65) | defp repo do

FILE: lib/error_tracker/schemas/error.ex
  class ErrorTracker.Error (line 1) | defmodule ErrorTracker.Error
    method new (line 42) | def new(kind, reason, %ErrorTracker.Stacktrace{} = stacktrace) do
    method has_source_info? (line 78) | def has_source_info?(%__MODULE__{source_function: "-", source_line: "-...
    method has_source_info? (line 79) | def has_source_info?(%__MODULE__{}), do: true

FILE: lib/error_tracker/schemas/occurrence.ex
  class ErrorTracker.Occurrence (line 1) | defmodule ErrorTracker.Occurrence
    method changeset (line 30) | def changeset(occurrence, attrs) do
    method validate_context (line 45) | defp validate_context(changeset) do
    method maybe_put_stacktrace (line 77) | defp maybe_put_stacktrace(changeset) do

FILE: lib/error_tracker/schemas/stacktrace.ex
  class ErrorTracker.Stacktrace (line 1) | defmodule ErrorTracker.Stacktrace
    method new (line 23) | def new(stack) do
    method line_changeset (line 47) | defp line_changeset(%__MODULE__.Line{} = line, params) do
    method source (line 57) | def source(%__MODULE__{} = stack) do

FILE: lib/error_tracker/telemetry.ex
  class ErrorTracker.Telemetry (line 1) | defmodule ErrorTracker.Telemetry
    method new_error (line 49) | def new_error(%ErrorTracker.Error{} = error) do
    method unresolved_error (line 56) | def unresolved_error(%ErrorTracker.Error{} = error) do
    method resolved_error (line 63) | def resolved_error(%ErrorTracker.Error{} = error) do

FILE: lib/error_tracker/web.ex
  class ErrorTracker.Web (line 1) | defmodule ErrorTracker.Web
    method html (line 72) | def html do
    method live_view (line 81) | def live_view do
    method live_component (line 90) | def live_component do
    method router (line 99) | def router do
    method html_helpers (line 105) | defp html_helpers do

FILE: lib/error_tracker/web/components/core_components.ex
  class ErrorTracker.Web.CoreComponents (line 1) | defmodule ErrorTracker.Web.CoreComponents
    method button (line 19) | def button(%{type: "link"} = assigns) do
    method button (line 34) | def button(assigns) do
    method badge (line 63) | def badge(assigns) do
    method pagination (line 93) | def pagination(assigns) do
    method section (line 120) | def section(assigns) do
    method icon (line 136) | def icon(%{name: "bell"} = assigns) do
    method icon (line 153) | def icon(%{name: "bell-slash"} = assigns) do
    method icon (line 171) | def icon(%{name: "arrow-left"} = assigns) do
    method icon (line 188) | def icon(%{name: "arrow-right"} = assigns) do

FILE: lib/error_tracker/web/components/layouts.ex
  class ErrorTracker.Web.Layouts (line 1) | defmodule ErrorTracker.Web.Layouts
    method get_content (line 27) | def get_content(:css), do: @css
    method get_content (line 28) | def get_content(:js), do: @js
    method get_socket_config (line 30) | def get_socket_config(key) do
    method navbar (line 36) | def navbar(assigns) do
    method navbar_item (line 101) | def navbar_item(assigns) do

FILE: lib/error_tracker/web/helpers.ex
  class ErrorTracker.Web.Helpers (line 1) | defmodule ErrorTracker.Web.Helpers
    method sanitize_module (line 5) | def sanitize_module(<<"Elixir.", str::binary>>), do: str
    method sanitize_module (line 6) | def sanitize_module(str), do: str
    method format_datetime (line 9) | def format_datetime(%DateTime{} = dt), do: Calendar.strftime(dt, "%c %Z")

FILE: lib/error_tracker/web/hooks/set_assigns.ex
  class ErrorTracker.Web.Hooks.SetAssigns (line 1) | defmodule ErrorTracker.Web.Hooks.SetAssigns
    method on_mount (line 6) | def on_mount({:set_dashboard_path, path}, _params, session, socket) do

FILE: lib/error_tracker/web/live/dashboard.ex
  class ErrorTracker.Web.Live.Dashboard (line 1) | defmodule ErrorTracker.Web.Live.Dashboard
    method handle_params (line 15) | def handle_params(params, uri, socket) do
    method handle_event (line 30) | def handle_event("search", params, socket) do
    method handle_event (line 40) | def handle_event("next-page", _params, socket) do
    method handle_event (line 45) | def handle_event("prev-page", _params, socket) do
    method handle_event (line 50) | def handle_event("resolve", %{"error_id" => id}, socket) do
    method handle_event (line 58) | def handle_event("unresolve", %{"error_id" => id}, socket) do
    method handle_event (line 66) | def handle_event("mute", %{"error_id" => id}, socket) do
    method handle_event (line 74) | def handle_event("unmute", %{"error_id" => id}, socket) do
    method paginate_errors (line 81) | defp paginate_errors(socket) do
    method filter (line 117) | defp filter(query, search) do
    method do_filter (line 121) | defp do_filter({:status, status}, query) do
    method do_filter (line 125) | defp do_filter({field, value}, query) do

FILE: lib/error_tracker/web/live/show.ex
  class ErrorTracker.Web.Live.Show (line 1) | defmodule ErrorTracker.Web.Live.Show
    method mount (line 15) | def mount(%{"id" => id} = params, _session, socket) do
    method handle_params (line 27) | def handle_params(params, _uri, socket) do
    method handle_event (line 50) | def handle_event("occurrence_navigation", %{"occurrence_id" => id}, so...
    method handle_event (line 62) | def handle_event("resolve", _params, socket) do
    method handle_event (line 69) | def handle_event("unresolve", _params, socket) do
    method handle_event (line 76) | def handle_event("mute", _params, socket) do
    method handle_event (line 83) | def handle_event("unmute", _params, socket) do
    method load_related_occurrences (line 89) | defp load_related_occurrences(socket) do
    method related_occurrences (line 152) | defp related_occurrences(query, num_results) do

FILE: lib/error_tracker/web/router.ex
  class ErrorTracker.Web.Router (line 1) | defmodule ErrorTracker.Web.Router
    method __parse_options__ (line 57) | def __parse_options__(opts, path) do
    method __session__ (line 78) | def __session__(conn, csp_nonce_assign_key) do

FILE: lib/error_tracker/web/router/routes.ex
  class ErrorTracker.Web.Router.Routes (line 1) | defmodule ErrorTracker.Web.Router.Routes
    method dashboard_path (line 11) | def dashboard_path(%Socket{} = socket, params \\ %{}) do
    method error_path (line 20) | def error_path(%Socket{} = socket, %Error{id: id}, params \\ %{}) do
    method occurrence_path (line 30) | def occurrence_path(%Socket{} = socket, %Occurrence{id: id, error_id: ...
    method dashboard_uri (line 37) | defp dashboard_uri(%Socket{} = socket, params) do

FILE: lib/error_tracker/web/search.ex
  class ErrorTracker.Web.Search (line 1) | defmodule ErrorTracker.Web.Search
    method changeset (line 11) | defp changeset(params) do
    method from_params (line 16) | def from_params(params) do
    method to_form (line 21) | def to_form(params) do

FILE: lib/mix/tasks/error_tracker.install.ex
  class Mix.Tasks.ErrorTracker.Install.Docs (line 1) | defmodule Mix.Tasks.ErrorTracker.Install.Docs
    method short_doc (line 4) | def short_doc do
    method example (line 8) | def example do
    method long_doc (line 12) | def long_doc do

FILE: mix.exs
  class ErrorTracker.MixProject (line 1) | defmodule ErrorTracker.MixProject
    method project (line 4) | def project do
    method application (line 32) | def application do
    method elixirc_paths (line 39) | defp elixirc_paths(:test), do: ["lib", "test/support"]
    method elixirc_paths (line 40) | defp elixirc_paths(_env), do: ["lib"]
    method package (line 42) | def package do
    method description (line 57) | def description do
    method groups_for_modules (line 61) | defp groups_for_modules do
    method deps (line 85) | defp deps do
    method aliases (line 108) | defp aliases do

FILE: priv/repo/migrations/20240527155639_create_error_tracker_tables.exs
  class ErrorTracker.Repo.Migrations.CreateErrorTrackerTables (line 1) | defmodule ErrorTracker.Repo.Migrations.CreateErrorTrackerTables

FILE: priv/repo/seeds.exs
  class ErrorTrackerDev.Repo (line 7) | defmodule ErrorTrackerDev.Repo

FILE: priv/static/app.js
  function u (line 1) | function u(){t.width=n.innerWidth,t.height=5*i.barThickness;var e=t.getC...
  method mounted (line 1) | mounted(){this.formatJson()}
  method updated (line 1) | updated(){this.formatJson()}
  method formatJson (line 1) | formatJson(){try{const n=this.el.textContent.trim(),s=JSON.stringify(JSO...

FILE: test/error_tracker/filter_test.exs
  class ErrorTracker.FilterTest (line 1) | defmodule ErrorTracker.FilterTest
    class ErrorTracker.FilterTest.AuthHeaderHider (line 45) | defmodule ErrorTracker.FilterTest.AuthHeaderHider
      method sanitize (line 49) | def sanitize(context) do

FILE: test/error_tracker/ignorer_test.exs
  class ErrorTracker.IgnorerTest (line 1) | defmodule ErrorTracker.IgnorerTest
  class ErrorTracker.EveryErrorIgnorer (line 28) | defmodule ErrorTracker.EveryErrorIgnorer
    method ignore? (line 33) | def ignore?(error, _context) do

FILE: test/error_tracker/schemas/occurrence_test.exs
  class ErrorTracker.OccurrenceTest (line 1) | defmodule ErrorTracker.OccurrenceTest

FILE: test/error_tracker/telemetry_test.exs
  class ErrorTracker.TelemetryTest (line 1) | defmodule ErrorTracker.TelemetryTest

FILE: test/error_tracker_test.exs
  class ErrorTrackerTest (line 1) | defmodule ErrorTrackerTest
  class ErrorWithBreadcrumbs (line 174) | defmodule ErrorWithBreadcrumbs

FILE: test/integrations/plug_test.exs
  class ErrorTracker.Integrations.PlugTest (line 1) | defmodule ErrorTracker.Integrations.PlugTest

FILE: test/support/case.ex
  class ErrorTracker.Test.Case (line 1) | defmodule ErrorTracker.Test.Case
    method report_error (line 19) | def report_error(fun, context \\ %{}) do
    method attach_telemetry (line 44) | def attach_telemetry do
    method _send_telemetry (line 58) | def _send_telemetry(event, measurements, metadata, _opts) do
    method repo (line 62) | def repo do

FILE: test/support/lite_repo.ex
  class ErrorTracker.Test.LiteRepo (line 1) | defmodule ErrorTracker.Test.LiteRepo

FILE: test/support/mysql_repo.ex
  class ErrorTracker.Test.MySQLRepo (line 1) | defmodule ErrorTracker.Test.MySQLRepo

FILE: test/support/repo.ex
  class ErrorTracker.Test.Repo (line 1) | defmodule ErrorTracker.Test.Repo
Condensed preview — 80 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (212K chars).
[
  {
    "path": ".formatter.exs",
    "chars": 694,
    "preview": "# Used by \"mix format\"\nlocals_without_parens = [error_tracker_dashboard: 1, error_tracker_dashboard: 2]\n\n# Parse SemVer "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 615,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 604,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is"
  },
  {
    "path": ".github/workflows/elixir.yml",
    "chars": 2568,
    "preview": "name: CI\non:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - 'guides/**'\n  pull_request:\n    paths-ignore:\n"
  },
  {
    "path": ".gitignore",
    "chars": 800,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": ".tool-versions",
    "chars": 24,
    "preview": "elixir 1.19\nerlang 28.1\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 118,
    "preview": "# Changelog\n\nPlease see [our GitHub \"Releases\" page](https://github.com/elixir-error-tracker/error-tracker/releases).\n"
  },
  {
    "path": "LICENSE",
    "chars": 11354,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 3109,
    "preview": "# 🐛 ErrorTracker\n\n<a title=\"GitHub CI\" href=\"https://github.com/elixir-error-tracker/error-tracker/actions\"><img src=\"ht"
  },
  {
    "path": "assets/css/app.css",
    "chars": 246,
    "preview": "@import \"tailwindcss/base\";\n@import \"tailwindcss/components\";\n@import \"tailwindcss/utilities\";\n\n::-webkit-scrollbar {\n  "
  },
  {
    "path": "assets/js/app.js",
    "chars": 1579,
    "preview": "// Phoenix assets are imported from dependencies.\nimport topbar from \"topbar\";\n\nlet csrfToken = document.querySelector(\""
  },
  {
    "path": "assets/package.json",
    "chars": 160,
    "preview": "{\n  \"workspaces\": [\n    \"../deps/*\"\n  ],\n  \"dependencies\": {\n    \"phoenix\": \"workspace:*\",\n    \"phoenix_live_view\": \"wor"
  },
  {
    "path": "assets/tailwind.config.js",
    "chars": 815,
    "preview": "// See the Tailwind configuration guide for advanced usage\n// https://tailwindcss.com/docs/configuration\n\nlet plugin = r"
  },
  {
    "path": "config/config.exs",
    "chars": 51,
    "preview": "import Config\n\nimport_config \"#{config_env()}.exs\"\n"
  },
  {
    "path": "config/dev.example.exs",
    "chars": 1291,
    "preview": "import Config\n\nconfig :bun,\n  version: \"1.1.18\",\n  default: [\n    args: ~w(build app.js --outdir=../../priv/static),\n   "
  },
  {
    "path": "config/test.example.exs",
    "chars": 764,
    "preview": "import Config\n\nalias Ecto.Adapters.SQL.Sandbox\nalias ErrorTracker.Test.Repo\n\nconfig :error_tracker, ErrorTracker.Test.Li"
  },
  {
    "path": "dev.exs",
    "chars": 7603,
    "preview": "# This is the development server for Errortracker built on the PhoenixLiveDashboard project.\n# To start the development "
  },
  {
    "path": "guides/Getting Started.md",
    "chars": 9121,
    "preview": "# Getting Started\n\nThis guide is an introduction to ErrorTracker, an Elixir-based built-in error tracking solution. Erro"
  },
  {
    "path": "lib/error_tracker/application.ex",
    "chars": 407,
    "preview": "defmodule ErrorTracker.Application do\n  @moduledoc false\n\n  use Application\n\n  def start(_type, _args) do\n    children ="
  },
  {
    "path": "lib/error_tracker/filter.ex",
    "chars": 1065,
    "preview": "defmodule ErrorTracker.Filter do\n  @moduledoc \"\"\"\n  Behaviour for sanitizing & modifying the error context before it's s"
  },
  {
    "path": "lib/error_tracker/ignorer.ex",
    "chars": 1840,
    "preview": "defmodule ErrorTracker.Ignorer do\n  @moduledoc \"\"\"\n  Behaviour for ignoring errors.\n\n  > #### Ignoring vs muting errors "
  },
  {
    "path": "lib/error_tracker/integrations/oban.ex",
    "chars": 2535,
    "preview": "defmodule ErrorTracker.Integrations.Oban do\n  @moduledoc \"\"\"\n  Integration with Oban.\n\n  ## How to use it\n\n  It is a plu"
  },
  {
    "path": "lib/error_tracker/integrations/phoenix.ex",
    "chars": 4670,
    "preview": "defmodule ErrorTracker.Integrations.Phoenix do\n  @moduledoc \"\"\"\n  Integration with Phoenix applications.\n\n  ## How to us"
  },
  {
    "path": "lib/error_tracker/integrations/plug.ex",
    "chars": 3942,
    "preview": "defmodule ErrorTracker.Integrations.Plug do\n  @moduledoc \"\"\"\n  Integration with Plug applications.\n\n  ## How to use it\n\n"
  },
  {
    "path": "lib/error_tracker/migration/mysql/v03.ex",
    "chars": 1505,
    "preview": "defmodule ErrorTracker.Migration.MySQL.V03 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(_opts) do\n    create ta"
  },
  {
    "path": "lib/error_tracker/migration/mysql/v04.ex",
    "chars": 323,
    "preview": "defmodule ErrorTracker.Migration.MySQL.V04 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(_opts) do\n    alter tab"
  },
  {
    "path": "lib/error_tracker/migration/mysql/v05.ex",
    "chars": 321,
    "preview": "defmodule ErrorTracker.Migration.MySQL.V05 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(_opts) do\n    alter tab"
  },
  {
    "path": "lib/error_tracker/migration/mysql.ex",
    "chars": 794,
    "preview": "defmodule ErrorTracker.Migration.MySQL do\n  @moduledoc false\n\n  @behaviour ErrorTracker.Migration\n\n  use Ecto.Migration\n"
  },
  {
    "path": "lib/error_tracker/migration/postgres/v01.ex",
    "chars": 2943,
    "preview": "defmodule ErrorTracker.Migration.Postgres.V01 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  import Ecto.Query\n\n  def up"
  },
  {
    "path": "lib/error_tracker/migration/postgres/v02.ex",
    "chars": 876,
    "preview": "defmodule ErrorTracker.Migration.Postgres.V02 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(%{prefix: prefix}) d"
  },
  {
    "path": "lib/error_tracker/migration/postgres/v03.ex",
    "chars": 352,
    "preview": "defmodule ErrorTracker.Migration.Postgres.V03 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(%{prefix: prefix}) d"
  },
  {
    "path": "lib/error_tracker/migration/postgres/v04.ex",
    "chars": 408,
    "preview": "defmodule ErrorTracker.Migration.Postgres.V04 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(%{prefix: prefix}) d"
  },
  {
    "path": "lib/error_tracker/migration/postgres/v05.ex",
    "chars": 380,
    "preview": "defmodule ErrorTracker.Migration.Postgres.V05 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(%{prefix: prefix}) d"
  },
  {
    "path": "lib/error_tracker/migration/postgres.ex",
    "chars": 1091,
    "preview": "defmodule ErrorTracker.Migration.Postgres do\n  @moduledoc false\n\n  @behaviour ErrorTracker.Migration\n\n  use Ecto.Migrati"
  },
  {
    "path": "lib/error_tracker/migration/sql_migrator.ex",
    "chars": 3047,
    "preview": "defmodule ErrorTracker.Migration.SQLMigrator do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  import Ecto.Query\n\n  alias E"
  },
  {
    "path": "lib/error_tracker/migration/sqlite/v02.ex",
    "chars": 1442,
    "preview": "defmodule ErrorTracker.Migration.SQLite.V02 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(_opts) do\n    create t"
  },
  {
    "path": "lib/error_tracker/migration/sqlite/v03.ex",
    "chars": 294,
    "preview": "defmodule ErrorTracker.Migration.SQLite.V03 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(_opts) do\n    create_i"
  },
  {
    "path": "lib/error_tracker/migration/sqlite/v04.ex",
    "chars": 350,
    "preview": "defmodule ErrorTracker.Migration.SQLite.V04 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(_opts) do\n    alter ta"
  },
  {
    "path": "lib/error_tracker/migration/sqlite/v05.ex",
    "chars": 322,
    "preview": "defmodule ErrorTracker.Migration.SQLite.V05 do\n  @moduledoc false\n\n  use Ecto.Migration\n\n  def up(_opts) do\n    alter ta"
  },
  {
    "path": "lib/error_tracker/migration/sqlite.ex",
    "chars": 795,
    "preview": "defmodule ErrorTracker.Migration.SQLite do\n  @moduledoc false\n\n  @behaviour ErrorTracker.Migration\n\n  use Ecto.Migration"
  },
  {
    "path": "lib/error_tracker/migration.ex",
    "chars": 3470,
    "preview": "defmodule ErrorTracker.Migration do\n  @moduledoc \"\"\"\n  Create and modify the database tables for ErrorTracker.\n\n  ## Usa"
  },
  {
    "path": "lib/error_tracker/plugins/pruner.ex",
    "chars": 4499,
    "preview": "defmodule ErrorTracker.Plugins.Pruner do\n  @moduledoc \"\"\"\n  Periodically delete resolved errors based on their age.\n\n  P"
  },
  {
    "path": "lib/error_tracker/repo.ex",
    "chars": 1532,
    "preview": "defmodule ErrorTracker.Repo do\n  @moduledoc false\n\n  def insert!(struct_or_changeset, opts \\\\ []) do\n    dispatch(:inser"
  },
  {
    "path": "lib/error_tracker/schemas/error.ex",
    "chars": 2587,
    "preview": "defmodule ErrorTracker.Error do\n  @moduledoc \"\"\"\n  Schema to store an error or exception recorded by ErrorTracker.\n\n  It"
  },
  {
    "path": "lib/error_tracker/schemas/occurrence.ex",
    "chars": 2288,
    "preview": "defmodule ErrorTracker.Occurrence do\n  @moduledoc \"\"\"\n  Schema to store a particular instance of an error in a given tim"
  },
  {
    "path": "lib/error_tracker/schemas/stacktrace.ex",
    "chars": 2210,
    "preview": "defmodule ErrorTracker.Stacktrace do\n  @moduledoc \"\"\"\n  An Stacktrace contains the information about the execution stack"
  },
  {
    "path": "lib/error_tracker/telemetry.ex",
    "chars": 2985,
    "preview": "defmodule ErrorTracker.Telemetry do\n  @moduledoc \"\"\"\n  Telemetry events of ErrorTracker.\n\n  ErrorTracker emits some even"
  },
  {
    "path": "lib/error_tracker/web/components/core_components.ex",
    "chars": 5573,
    "preview": "defmodule ErrorTracker.Web.CoreComponents do\n  @moduledoc false\n  use Phoenix.Component\n\n  @doc \"\"\"\n  Renders a button.\n"
  },
  {
    "path": "lib/error_tracker/web/components/layouts/live.html.heex",
    "chars": 109,
    "preview": "<.navbar id=\"navbar\" {assigns} />\n<main class=\"container px-4 mx-auto mt-4 mb-4\">\n  {@inner_content}\n</main>\n"
  },
  {
    "path": "lib/error_tracker/web/components/layouts/root.html.heex",
    "chars": 738,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"I"
  },
  {
    "path": "lib/error_tracker/web/components/layouts.ex",
    "chars": 4369,
    "preview": "defmodule ErrorTracker.Web.Layouts do\n  @moduledoc false\n  use ErrorTracker.Web, :html\n\n  phoenix_js_paths =\n    for app"
  },
  {
    "path": "lib/error_tracker/web/helpers.ex",
    "chars": 260,
    "preview": "defmodule ErrorTracker.Web.Helpers do\n  @moduledoc false\n\n  @doc false\n  def sanitize_module(<<\"Elixir.\", str::binary>>)"
  },
  {
    "path": "lib/error_tracker/web/hooks/set_assigns.ex",
    "chars": 342,
    "preview": "defmodule ErrorTracker.Web.Hooks.SetAssigns do\n  @moduledoc false\n\n  import Phoenix.Component, only: [assign: 2]\n\n  def "
  },
  {
    "path": "lib/error_tracker/web/live/dashboard.ex",
    "chars": 3658,
    "preview": "defmodule ErrorTracker.Web.Live.Dashboard do\n  @moduledoc false\n\n  use ErrorTracker.Web, :live_view\n\n  import Ecto.Query"
  },
  {
    "path": "lib/error_tracker/web/live/dashboard.html.heex",
    "chars": 4570,
    "preview": "<.form\n  for={@search_form}\n  id=\"search\"\n  class=\"mb-4 text-black grid md:grid-cols-4 grid-cols-2 gap-2\"\n  phx-change=\""
  },
  {
    "path": "lib/error_tracker/web/live/show.ex",
    "chars": 4372,
    "preview": "defmodule ErrorTracker.Web.Live.Show do\n  @moduledoc false\n  use ErrorTracker.Web, :live_view\n\n  import Ecto.Query\n\n  al"
  },
  {
    "path": "lib/error_tracker/web/live/show.html.heex",
    "chars": 5845,
    "preview": "<div class=\"my-6\">\n  <.link navigate={dashboard_path(@socket, @search)}>\n    <.icon name=\"arrow-left\" /> Back to the das"
  },
  {
    "path": "lib/error_tracker/web/router/routes.ex",
    "chars": 1048,
    "preview": "defmodule ErrorTracker.Web.Router.Routes do\n  @moduledoc false\n\n  alias ErrorTracker.Error\n  alias ErrorTracker.Occurren"
  },
  {
    "path": "lib/error_tracker/web/router.ex",
    "chars": 3016,
    "preview": "defmodule ErrorTracker.Web.Router do\n  @moduledoc \"\"\"\n  ErrorTracker UI integration into your application's router.\n  \"\""
  },
  {
    "path": "lib/error_tracker/web/search.ex",
    "chars": 572,
    "preview": "defmodule ErrorTracker.Web.Search do\n  @moduledoc false\n\n  @types %{\n    reason: :string,\n    source_line: :string,\n    "
  },
  {
    "path": "lib/error_tracker/web.ex",
    "chars": 3018,
    "preview": "defmodule ErrorTracker.Web do\n  @moduledoc \"\"\"\n  ErrorTracker includes a dashboard to view and inspect errors that occur"
  },
  {
    "path": "lib/error_tracker.ex",
    "chars": 13074,
    "preview": "defmodule ErrorTracker do\n  @moduledoc \"\"\"\n  En Elixir-based built-in error tracking solution.\n\n  The main objectives be"
  },
  {
    "path": "lib/mix/tasks/error_tracker.install.ex",
    "chars": 4344,
    "preview": "defmodule Mix.Tasks.ErrorTracker.Install.Docs do\n  @moduledoc false\n\n  def short_doc do\n    \"Install and configure Error"
  },
  {
    "path": "mix.exs",
    "chars": 3076,
    "preview": "defmodule ErrorTracker.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :error_tracker,\n      version:"
  },
  {
    "path": "priv/repo/migrations/20240527155639_create_error_tracker_tables.exs",
    "chars": 185,
    "preview": "defmodule ErrorTracker.Repo.Migrations.CreateErrorTrackerTables do\n  use Ecto.Migration\n\n  defdelegate up, to: ErrorTrac"
  },
  {
    "path": "priv/repo/seeds.exs",
    "chars": 1112,
    "preview": "adapter =\n  case Application.get_env(:error_tracker, :ecto_adapter) do\n    :postgres -> Ecto.Adapters.Postgres\n    :sqli"
  },
  {
    "path": "priv/static/app.css",
    "chars": 28557,
    "preview": "/*\n! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com\n*/\n\n/*\n1. Prevent padding and border from affecting elem"
  },
  {
    "path": "priv/static/app.js",
    "chars": 3190,
    "preview": "var C=Object.create;var{defineProperty:b,getPrototypeOf:x,getOwnPropertyNames:E}=Object;var F=Object.prototype.hasOwnPro"
  },
  {
    "path": "test/error_tracker/filter_test.exs",
    "chars": 1676,
    "preview": "defmodule ErrorTracker.FilterTest do\n  use ErrorTracker.Test.Case\n\n  setup context do\n    if filter = context[:filter] d"
  },
  {
    "path": "test/error_tracker/ignorer_test.exs",
    "chars": 1160,
    "preview": "defmodule ErrorTracker.IgnorerTest do\n  use ErrorTracker.Test.Case\n\n  setup context do\n    if ignorer = context[:ignorer"
  },
  {
    "path": "test/error_tracker/schemas/occurrence_test.exs",
    "chars": 1155,
    "preview": "defmodule ErrorTracker.OccurrenceTest do\n  use ErrorTracker.Test.Case\n\n  import Ecto.Changeset\n\n  alias ErrorTracker.Occ"
  },
  {
    "path": "test/error_tracker/telemetry_test.exs",
    "chars": 1978,
    "preview": "defmodule ErrorTracker.TelemetryTest do\n  use ErrorTracker.Test.Case\n\n  alias ErrorTracker.Error\n  alias ErrorTracker.Oc"
  },
  {
    "path": "test/error_tracker_test.exs",
    "chars": 5874,
    "preview": "defmodule ErrorTrackerTest do\n  use ErrorTracker.Test.Case\n\n  alias ErrorTracker.Error\n  alias ErrorTracker.Occurrence\n\n"
  },
  {
    "path": "test/integrations/plug_test.exs",
    "chars": 1714,
    "preview": "defmodule ErrorTracker.Integrations.PlugTest do\n  use ErrorTracker.Test.Case\n\n  alias ErrorTracker.Integrations.Plug, as"
  },
  {
    "path": "test/support/case.ex",
    "chars": 1517,
    "preview": "defmodule ErrorTracker.Test.Case do\n  @moduledoc false\n  use ExUnit.CaseTemplate\n\n  using do\n    quote do\n      import E"
  },
  {
    "path": "test/support/lite_repo.ex",
    "chars": 136,
    "preview": "defmodule ErrorTracker.Test.LiteRepo do\n  @moduledoc false\n  use Ecto.Repo, otp_app: :error_tracker, adapter: Ecto.Adapt"
  },
  {
    "path": "test/support/mysql_repo.ex",
    "chars": 135,
    "preview": "defmodule ErrorTracker.Test.MySQLRepo do\n  @moduledoc false\n  use Ecto.Repo, otp_app: :error_tracker, adapter: Ecto.Adap"
  },
  {
    "path": "test/support/repo.ex",
    "chars": 133,
    "preview": "defmodule ErrorTracker.Test.Repo do\n  @moduledoc false\n  use Ecto.Repo, otp_app: :error_tracker, adapter: Ecto.Adapters."
  },
  {
    "path": "test/test_helper.exs",
    "chars": 673,
    "preview": "# Use the appropriate repo for the desired database\nrepo =\n  case System.get_env(\"DB\") do\n    \"sqlite\" ->\n      ErrorTra"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the elixir-error-tracker/error-tracker GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 80 files (193.3 KB), approximately 55.2k tokens, and a symbol index with 274 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!