From c1a1d697a45bf1c6764eabe35f666e7474b5ad04 Mon Sep 17 00:00:00 2001 From: hq1 Date: Tue, 9 Jan 2024 17:17:42 +0100 Subject: [PATCH] Partially revert #3661 - just keep the real errors wrapped, but don't display anything to the user (#3677) * Revert "Remove unused RocketIcon" This reverts commit c5e8d0c1727fcc14d95e79234b169d2615ea2cf8. * Revert "Display either hash or actual error message" This reverts commit 0c091ab35f1f979f7e8770d1c5acb01207608ea8. * Revert "Use ApiErrorNotice in funnels" This reverts commit 5929de248eba79e655cde5b1ab1eaa9ebb687332. * Revert "Don't render "No data yet" when there's a NetworkError for example" This reverts commit 70bee07632fe435f8b62a5a2687aef63200d02c6. * Revert "Show the sinking shuttle notice whenever an API error occurs" This reverts commit 9a62c8af2bb9a2517e0eeac8819b4cf34e642210. * Revert "Add Hahash dependency" This reverts commit b94207ea0aae6c8e90b9923f2fdf871bbe45c7c3. * Remove support hash --- assets/js/dashboard/api.js | 43 +++-------- assets/js/dashboard/extra/funnel.js | 11 ++- .../dashboard/stats/behaviours/conversions.js | 52 +++++-------- assets/js/dashboard/stats/behaviours/props.js | 73 +++++++------------ assets/js/dashboard/stats/locations/map.js | 28 +++---- .../js/dashboard/stats/modals/conversions.js | 9 +-- .../js/dashboard/stats/modals/entry-pages.js | 7 +- .../js/dashboard/stats/modals/exit-pages.js | 8 +- assets/js/dashboard/stats/modals/pages.js | 7 +- assets/js/dashboard/stats/modals/props.js | 9 +-- assets/js/dashboard/stats/modals/sources.js | 7 +- assets/js/dashboard/stats/modals/table.js | 21 ++---- assets/js/dashboard/stats/reports/list.js | 8 +- lib/plausible_web/plugs/error_handler.ex | 4 +- mix.exs | 3 +- mix.lock | 1 - 16 files changed, 100 insertions(+), 191 deletions(-) diff --git a/assets/js/dashboard/api.js b/assets/js/dashboard/api.js index 689ef0c08..158f9a60f 100644 --- a/assets/js/dashboard/api.js +++ b/assets/js/dashboard/api.js @@ -1,6 +1,4 @@ -import { formatISO } from './util/date' -import React from 'react'; -import RocketIcon from './stats/modals/rocket-icon' +import {formatISO} from './util/date' let abortController = new AbortController() let SHARED_LINK_AUTH = null @@ -13,23 +11,6 @@ class ApiError extends Error { } } -export function ApiErrorNotice({ error }) { - return ( -
-
- -
Oops! Our servers had trouble retrieving your data.
-
If the problem persists after refreshing your browser, please contact support with the following code: -
-
- {!error.payload && error.message} - {error.payload && error.payload.support_hash} -
-
-
- ); -}; - function serialize(obj) { var str = []; for (var p in obj) @@ -55,14 +36,14 @@ function serializeFilters(filters) { return JSON.stringify(cleaned) } -export function serializeQuery(query, extraQuery = []) { +export function serializeQuery(query, extraQuery=[]) { const queryObj = {} - if (query.period) { queryObj.period = query.period } - if (query.date) { queryObj.date = formatISO(query.date) } - if (query.from) { queryObj.from = formatISO(query.from) } - if (query.to) { queryObj.to = formatISO(query.to) } - if (query.filters) { queryObj.filters = serializeFilters(query.filters) } - if (query.with_imported) { queryObj.with_imported = query.with_imported } + if (query.period) { queryObj.period = query.period } + if (query.date) { queryObj.date = formatISO(query.date) } + if (query.from) { queryObj.from = formatISO(query.from) } + if (query.to) { queryObj.to = formatISO(query.to) } + if (query.filters) { queryObj.filters = serializeFilters(query.filters) } + if (query.with_imported) { queryObj.with_imported = query.with_imported } if (SHARED_LINK_AUTH) { queryObj.auth = SHARED_LINK_AUTH } if (query.comparison) { @@ -77,11 +58,11 @@ export function serializeQuery(query, extraQuery = []) { return '?' + serialize(queryObj) } -export function get(url, query = {}, ...extraQuery) { - const headers = SHARED_LINK_AUTH ? { 'X-Shared-Link-Auth': SHARED_LINK_AUTH } : {} +export function get(url, query={}, ...extraQuery) { + const headers = SHARED_LINK_AUTH ? {'X-Shared-Link-Auth': SHARED_LINK_AUTH} : {} url = url + serializeQuery(query, extraQuery) - return fetch(url, { signal: abortController.signal, headers: headers }) - .then(response => { + return fetch(url, {signal: abortController.signal, headers: headers}) + .then( response => { if (!response.ok) { return response.json().then((msg) => { throw new ApiError(msg.error, msg) diff --git a/assets/js/dashboard/extra/funnel.js b/assets/js/dashboard/extra/funnel.js index 50686f704..2f598cdaf 100644 --- a/assets/js/dashboard/extra/funnel.js +++ b/assets/js/dashboard/extra/funnel.js @@ -5,11 +5,13 @@ import FunnelTooltip from './funnel-tooltip.js' import ChartDataLabels from 'chartjs-plugin-datalabels' import numberFormatter from '../util/number-formatter' import Bar from '../stats/bar' -import { ApiErrorNotice } from '../api' + +import RocketIcon from '../stats/modals/rocket-icon' import * as api from '../api' import LazyLoader from '../components/lazy-loader' + export default function Funnel(props) { const [loading, setLoading] = useState(true) const [visible, setVisible] = useState(false) @@ -273,7 +275,12 @@ export default function Funnel(props) { return ( <> {header()} - +
+ +
Oops! Something went wrong
+
{error.message ? error.message : 'Failed to render funnel'}
+
Please try refreshing your browser or selecting the funnel again.
+
) } diff --git a/assets/js/dashboard/stats/behaviours/conversions.js b/assets/js/dashboard/stats/behaviours/conversions.js index 2ec2ba932..6f2fe1387 100644 --- a/assets/js/dashboard/stats/behaviours/conversions.js +++ b/assets/js/dashboard/stats/behaviours/conversions.js @@ -2,21 +2,15 @@ import React from 'react'; import * as api from '../../api' import * as url from '../../util/url' import { escapeFilterValue } from '../../util/filters' -import { ApiErrorNotice } from '../../api' - import { CR_METRIC } from '../reports/metrics'; import ListReport from '../reports/list'; export default function Conversions(props) { const { site, query } = props - const [error, setError] = React.useState(undefined) function fetchConversions() { return api.get(url.apiPath(site, '/conversions'), query, { limit: 9 }) - .catch((err) => { - setError(err) - }) } function getFilterFor(listItem) { @@ -24,30 +18,24 @@ export default function Conversions(props) { } /*global BUILD_EXTRA*/ - if (error) { - return ( - - ) - } else { - return ( - - ) - } + return ( + + ) } diff --git a/assets/js/dashboard/stats/behaviours/props.js b/assets/js/dashboard/stats/behaviours/props.js index 29a77c2a4..47167bb72 100644 --- a/assets/js/dashboard/stats/behaviours/props.js +++ b/assets/js/dashboard/stats/behaviours/props.js @@ -6,13 +6,13 @@ import * as url from '../../util/url' import { CR_METRIC, PERCENTAGE_METRIC } from "../reports/metrics"; import * as storage from "../../util/storage"; import { parsePrefix, escapeFilterValue } from "../../util/filters" -import { ApiErrorNotice } from '../../api' + export default function Properties(props) { const { site, query } = props const propKeyStorageName = `prop_key__${site.domain}` const propKeyStorageNameForGoal = `${query.filters.goal}__prop_key__${site.domain}` - const [error, setError] = useState(undefined) + const [propKey, setPropKey] = useState(choosePropKey()) useEffect(() => { @@ -48,17 +48,11 @@ export default function Properties(props) { function fetchProps() { return api.get(url.apiPath(site, `/custom-prop-values/${encodeURIComponent(propKey)}`), query) - .catch((err) => { - setError(err) - }) } const fetchPropKeyOptions = useCallback(() => { return (input) => { return api.get(url.apiPath(site, "/suggestions/prop_key"), query, { q: input.trim() }) - .catch((err) => { - setError(err) - }) } }, [query]) @@ -77,48 +71,37 @@ export default function Properties(props) { /*global BUILD_EXTRA*/ function renderBreakdown() { - if (error) { - return ( - - ) - } else { - return ( - - ) - } + return ( + + ) } const getFilterFor = (listItem) => { return { 'props': JSON.stringify({ [propKey]: escapeFilterValue(listItem.name) }) } } const comboboxValues = propKey ? [{ value: propKey, label: propKey }] : [] const boxClass = 'pl-2 pr-8 py-1 bg-transparent dark:text-gray-300 rounded-md shadow-sm border border-gray-300 dark:border-gray-500' - if (error) { - return () - - } else { - return ( -
-
- -
- {propKey && renderBreakdown()} + return ( +
+
+
- ) - } + {propKey && renderBreakdown()} +
+ ) } diff --git a/assets/js/dashboard/stats/locations/map.js b/assets/js/dashboard/stats/locations/map.js index b326f679c..a6c2f8dbf 100644 --- a/assets/js/dashboard/stats/locations/map.js +++ b/assets/js/dashboard/stats/locations/map.js @@ -10,8 +10,6 @@ import MoreLink from '../more-link' import * as api from '../../api' import { navigateToQuery } from '../../query' -import { ApiErrorNotice } from '../../api' - class Countries extends React.Component { constructor(props) { super(props) @@ -29,7 +27,7 @@ class Countries extends React.Component { componentDidUpdate(prevProps) { if (this.props.query !== prevProps.query) { // eslint-disable-next-line react/no-did-update-set-state - this.setState({ loading: true, countries: null }) + this.setState({loading: true, countries: null}) this.fetchCountries().then(this.drawMap) } } @@ -50,19 +48,19 @@ class Countries extends React.Component { getDataset() { const dataset = {}; - var onlyValues = this.state.countries.map(function(obj) { return obj.visitors }); + var onlyValues = this.state.countries.map(function(obj){ return obj.visitors }); var maxValue = Math.max.apply(null, onlyValues); // eslint-disable-next-line no-undef const paletteScale = d3.scale.linear() - .domain([0, maxValue]) + .domain([0,maxValue]) .range([ this.state.darkTheme ? "#2e3954" : "#f3ebff", this.state.darkTheme ? "#6366f1" : "#a779e9" ]) - this.state.countries.forEach(function(item) { - dataset[item.alpha_3] = { numberOfThings: item.visitors, fillColor: paletteScale(item.visitors) }; + this.state.countries.forEach(function(item){ + dataset[item.alpha_3] = {numberOfThings: item.visitors, fillColor: paletteScale(item.visitors)}; }); return dataset @@ -70,14 +68,13 @@ class Countries extends React.Component { updateCountries() { this.fetchCountries().then(() => { - this.map.updateChoropleth(this.getDataset(), { reset: true }) + this.map.updateChoropleth(this.getDataset(), {reset: true}) }) } fetchCountries() { - return api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.props.query, { limit: 300 }) - .then((res) => this.setState({ loading: false, countries: res })) - .catch((err) => this.setState({ loading: false, error: err })) + return api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.props.query, {limit: 300}) + .then((res) => this.setState({loading: false, countries: res})) } resizeMap() { @@ -148,9 +145,9 @@ class Countries extends React.Component { if (this.state.countries) { return ( <> -
+
- {this.geolocationDbNotice()} + { this.geolocationDbNotice() } ) } @@ -161,10 +158,9 @@ class Countries extends React.Component { render() { return ( - {this.state.loading &&
} - {this.state.error && } + { this.state.loading &&
} - {this.renderBody()} + { this.renderBody() }
) diff --git a/assets/js/dashboard/stats/modals/conversions.js b/assets/js/dashboard/stats/modals/conversions.js index 7b85b6e53..2f0254ceb 100644 --- a/assets/js/dashboard/stats/modals/conversions.js +++ b/assets/js/dashboard/stats/modals/conversions.js @@ -8,7 +8,6 @@ import * as url from "../../util/url"; import numberFormatter from '../../util/number-formatter' import { parseQuery } from '../../query' import { escapeFilterValue } from '../../util/filters' -import { ApiErrorNotice } from '../../api' /*global BUILD_EXTRA*/ /*global require*/ @@ -25,7 +24,7 @@ const Money = maybeRequire().default function ConversionsModal(props) { const site = props.site const query = parseQuery(props.location.search, site) - const [error, setError] = useState(undefined) + const [loading, setLoading] = useState(true) const [moreResultsAvailable, setMoreResultsAvailable] = useState(false) const [page, setPage] = useState(1) @@ -43,9 +42,6 @@ function ConversionsModal(props) { setPage(page + 1) setMoreResultsAvailable(res.length >= 100) }) - .catch((err) => { - setError(err) - }) } function loadMore() { @@ -124,8 +120,7 @@ function ConversionsModal(props) { return ( {renderBody()} - {error && } - {!error && loading && renderLoading()} + {loading && renderLoading()} {!loading && moreResultsAvailable && renderLoadMore()} ) diff --git a/assets/js/dashboard/stats/modals/entry-pages.js b/assets/js/dashboard/stats/modals/entry-pages.js index b44b662f8..a31353e75 100644 --- a/assets/js/dashboard/stats/modals/entry-pages.js +++ b/assets/js/dashboard/stats/modals/entry-pages.js @@ -8,8 +8,6 @@ import numberFormatter, { durationFormatter } from '../../util/number-formatter' import { parseQuery } from '../../query' import { trimURL } from '../../util/url' -import { ApiErrorNotice } from '../../api' - class EntryPagesModal extends React.Component { constructor(props) { super(props) @@ -18,8 +16,7 @@ class EntryPagesModal extends React.Component { query: parseQuery(props.location.search, props.site), pages: [], page: 1, - moreResultsAvailable: false, - error: undefined + moreResultsAvailable: false } } @@ -42,7 +39,6 @@ class EntryPagesModal extends React.Component { moreResultsAvailable: res.length === 100 })) ) - .catch((err) => this.setState({ loading: false, error: err })) } loadMore() { @@ -154,7 +150,6 @@ class EntryPagesModal extends React.Component { return ( {this.renderBody()} - {this.state.error && } {this.renderLoading()} ) diff --git a/assets/js/dashboard/stats/modals/exit-pages.js b/assets/js/dashboard/stats/modals/exit-pages.js index 3d5385078..da2135d79 100644 --- a/assets/js/dashboard/stats/modals/exit-pages.js +++ b/assets/js/dashboard/stats/modals/exit-pages.js @@ -7,9 +7,6 @@ import * as api from '../../api' import numberFormatter from '../../util/number-formatter' import { parseQuery } from '../../query' import { trimURL } from '../../util/url' - -import { ApiErrorNotice } from '../../api' - class ExitPagesModal extends React.Component { constructor(props) { super(props) @@ -18,8 +15,7 @@ class ExitPagesModal extends React.Component { query: parseQuery(props.location.search, props.site), pages: [], page: 1, - moreResultsAvailable: false, - error: undefined + moreResultsAvailable: false } } @@ -32,7 +28,6 @@ class ExitPagesModal extends React.Component { api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/exit-pages`, query, { limit: 100, page }) .then((res) => this.setState((state) => ({ loading: false, pages: state.pages.concat(res), moreResultsAvailable: res.length === 100 }))) - .catch((err) => this.setState({ loading: false, error: err })) } loadMore() { @@ -132,7 +127,6 @@ class ExitPagesModal extends React.Component { return ( {this.renderBody()} - {this.state.error && } {this.renderLoading()} ) diff --git a/assets/js/dashboard/stats/modals/pages.js b/assets/js/dashboard/stats/modals/pages.js index 69975d649..bf2538471 100644 --- a/assets/js/dashboard/stats/modals/pages.js +++ b/assets/js/dashboard/stats/modals/pages.js @@ -8,8 +8,6 @@ import numberFormatter, { durationFormatter } from '../../util/number-formatter' import { parseQuery } from '../../query' import { trimURL } from '../../util/url' -import { ApiErrorNotice } from '../../api' - class PagesModal extends React.Component { constructor(props) { super(props) @@ -18,8 +16,7 @@ class PagesModal extends React.Component { query: parseQuery(props.location.search, props.site), pages: [], page: 1, - moreResultsAvailable: false, - error: undefined + moreResultsAvailable: false } } @@ -33,7 +30,6 @@ class PagesModal extends React.Component { api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/pages`, query, { limit: 100, page, detailed }) .then((res) => this.setState((state) => ({ loading: false, pages: state.pages.concat(res), moreResultsAvailable: res.length === 100 }))) - .catch((err) => this.setState({ loading: false, error: err })) } loadMore() { @@ -140,7 +136,6 @@ class PagesModal extends React.Component { return ( {this.renderBody()} - {this.state.error && } {this.renderLoading()} ) diff --git a/assets/js/dashboard/stats/modals/props.js b/assets/js/dashboard/stats/modals/props.js index 5a1543f4f..66ec9323b 100644 --- a/assets/js/dashboard/stats/modals/props.js +++ b/assets/js/dashboard/stats/modals/props.js @@ -9,7 +9,6 @@ import numberFormatter from '../../util/number-formatter' import { parseQuery } from '../../query' import { specialTitleWhenGoalFilter } from "../behaviours/goal-conversions"; import { escapeFilterValue } from "../../util/filters" -import { ApiErrorNotice } from '../../api' /*global BUILD_EXTRA*/ /*global require*/ @@ -27,7 +26,7 @@ function PropsModal(props) { const site = props.site const query = parseQuery(props.location.search, site) const propKey = props.location.pathname.split('/').pop() - const [error, setError] = useState(undefined) + const [loading, setLoading] = useState(true) const [moreResultsAvailable, setMoreResultsAvailable] = useState(false) const [page, setPage] = useState(1) @@ -45,9 +44,6 @@ function PropsModal(props) { setPage(page + 1) setMoreResultsAvailable(res.length >= 100) }) - .catch((err) => { - setError(err) - }) } function loadMore() { @@ -127,8 +123,7 @@ function PropsModal(props) { return ( {renderBody()} - {error && } - {!error && loading && renderLoading()} + {loading && renderLoading()} {!loading && moreResultsAvailable && renderLoadMore()} ) diff --git a/assets/js/dashboard/stats/modals/sources.js b/assets/js/dashboard/stats/modals/sources.js index 67ea0989f..aa2273ef1 100644 --- a/assets/js/dashboard/stats/modals/sources.js +++ b/assets/js/dashboard/stats/modals/sources.js @@ -6,8 +6,6 @@ import * as api from '../../api' import numberFormatter, { durationFormatter } from '../../util/number-formatter' import { parseQuery } from '../../query' -import { ApiErrorNotice } from '../../api' - const TITLES = { sources: 'Top Sources', utm_mediums: 'Top UTM mediums', @@ -25,8 +23,7 @@ class SourcesModal extends React.Component { sources: [], query: parseQuery(props.location.search, props.site), page: 1, - moreResultsAvailable: false, - error: undefined + moreResultsAvailable: false } } @@ -37,7 +34,6 @@ class SourcesModal extends React.Component { const detailed = this.showExtra() api.get(`/api/stats/${encodeURIComponent(site.domain)}/${this.currentFilter()}`, query, { limit: 100, page, detailed }) .then((res) => this.setState({ loading: false, sources: sources.concat(res), moreResultsAvailable: res.length === 100 })) - .catch((err) => this.setState({ loading: false, error: err })) } componentDidMount() { @@ -175,7 +171,6 @@ class SourcesModal extends React.Component { {this.renderLoading()} - {this.state.error && } ) } diff --git a/assets/js/dashboard/stats/modals/table.js b/assets/js/dashboard/stats/modals/table.js index 457c8fa6e..f97655e6b 100644 --- a/assets/js/dashboard/stats/modals/table.js +++ b/assets/js/dashboard/stats/modals/table.js @@ -4,24 +4,20 @@ import { Link, withRouter } from 'react-router-dom' import Modal from './modal' import * as api from '../../api' import numberFormatter from '../../util/number-formatter' -import { parseQuery } from '../../query' - -import { ApiErrorNotice } from '../../api' +import {parseQuery} from '../../query' class ModalTable extends React.Component { constructor(props) { super(props) this.state = { loading: true, - query: parseQuery(props.location.search, props.site), - error: undefined + query: parseQuery(props.location.search, props.site) } } componentDidMount() { - api.get(this.props.endpoint, this.state.query, { limit: 100 }) - .then((res) => this.setState({ loading: false, list: res })) - .catch((err) => this.setState({ loading: false, error: err })) + api.get(this.props.endpoint, this.state.query, {limit: 100}) + .then((res) => this.setState({loading: false, list: res})) } label() { @@ -38,7 +34,7 @@ class ModalTable extends React.Component { return ( - + {this.props.renderIcon && this.props.renderIcon(tableItem)} {this.props.renderIcon && ' '} {tableItem.name} @@ -47,7 +43,7 @@ class ModalTable extends React.Component { {numberFormatter(tableItem.visitors)} {tableItem.percentage >= 0 && - ({tableItem.percentage}%)} + ({tableItem.percentage}%) } ) @@ -86,7 +82,7 @@ class ModalTable extends React.Component { - {this.state.list.map(this.renderTableItem.bind(this))} + { this.state.list.map(this.renderTableItem.bind(this)) } @@ -100,8 +96,7 @@ class ModalTable extends React.Component { render() { return ( - {this.state.error && } - {this.renderBody()} + { this.renderBody() } ) } diff --git a/assets/js/dashboard/stats/reports/list.js b/assets/js/dashboard/stats/reports/list.js index 38c39c262..660884305 100644 --- a/assets/js/dashboard/stats/reports/list.js +++ b/assets/js/dashboard/stats/reports/list.js @@ -9,8 +9,6 @@ import Bar from '../bar' import LazyLoader from '../../components/lazy-loader' import classNames from 'classnames' import { trimURL } from '../../util/url' -import { ApiErrorNotice } from '../../api' - const MAX_ITEMS = 9 const MIN_HEIGHT = 380 const ROW_HEIGHT = 32 @@ -124,8 +122,6 @@ export default function ListReport(props) { } props.fetchData() .then((res) => setState({ loading: false, list: res })) - .catch((err) => setState({ loading: false, error: err })) - }, [props.keyLabel, props.query]) const onVisible = () => { setVisible(true) } @@ -180,9 +176,8 @@ export default function ListReport(props) { {maybeRenderDetailsLink()}
) - } else if (!state.error) { - return renderNoDataYet() } + return renderNoDataYet() } function renderReportHeader() { @@ -319,7 +314,6 @@ export default function ListReport(props) {
{state.loading && renderLoading()} - {state.error && } {!state.loading && {renderReport()} } diff --git a/lib/plausible_web/plugs/error_handler.ex b/lib/plausible_web/plugs/error_handler.ex index ef4c218c1..12168828a 100644 --- a/lib/plausible_web/plugs/error_handler.ex +++ b/lib/plausible_web/plugs/error_handler.ex @@ -13,9 +13,7 @@ defmodule PlausibleWeb.Plugs.ErrorHandler do @impl Plug.ErrorHandler def handle_errors(conn, %{kind: kind, reason: reason}) do - hash = Hahash.name({kind, reason}) - Sentry.Context.set_tags_context(%{hash: hash}) - json(conn, %{error: "internal server error", support_hash: hash}) + json(conn, %{error: "internal server error"}) end end end diff --git a/mix.exs b/mix.exs index 90a5baa8b..604142cb0 100644 --- a/mix.exs +++ b/mix.exs @@ -136,8 +136,7 @@ defmodule Plausible.MixProject do {:scrivener_ecto, "~> 2.0"}, {:esbuild, "~> 0.7", runtime: Mix.env() in [:dev, :small_dev]}, {:tailwind, "~> 0.2.0", runtime: Mix.env() in [:dev, :small_dev]}, - {:ex_json_logger, "~> 1.3.0"}, - {:hahash, "~> 0.2.0"} + {:ex_json_logger, "~> 1.3.0"} ] end diff --git a/mix.lock b/mix.lock index c0592e4a2..ed08d8590 100644 --- a/mix.lock +++ b/mix.lock @@ -61,7 +61,6 @@ "gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"}, "grpcbox": {:hex, :grpcbox, "0.16.0", "b83f37c62d6eeca347b77f9b1ec7e9f62231690cdfeb3a31be07cd4002ba9c82", [:rebar3], [{:acceptor_pool, "~>1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~>0.13.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~>0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~>0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "294df743ae20a7e030889f00644001370a4f7ce0121f3bbdaf13cf3169c62913"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "hahash": {:hex, :hahash, "0.2.0", "5d21c44b1b65d1f591adfd20ea4720e8a81db3caa377c28b20b434b8afc2115f", [:mix], [], "hexpm", "c39ce3f6163bbcf668e7a0bef88b608a9f37f99fcf181e7a17e4d10d02a5fbbd"}, "heroicons": {:hex, :heroicons, "0.5.3", "ee8ae8335303df3b18f2cc07f46e1cb6e761ba4cf2c901623fbe9a28c0bc51dd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a210037e8a09ac17e2a0a0779d729e89c821c944434c3baa7edfc1f5b32f3502"}, "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},