mirror of
https://github.com/penpot/penpot.git
synced 2026-02-12 14:42:56 +00:00
✨ Add several improvements to frontend error reporting
* ✨ Add major improvement on error handling * ✨ Add the ability to store frontend reports * 📎 Add PR feedback changes
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
|
||||
<style>
|
||||
{% include "app/templates/styles.css" %}
|
||||
</style>
|
||||
|
||||
@@ -5,14 +5,16 @@ penpot - error list
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<nav>
|
||||
<div class="title">
|
||||
<h1>Error reports (last 200)
|
||||
<a href="/dbg">[GO BACK]</a>
|
||||
</h1>
|
||||
<a href="/dbg"> [BACK]</a>
|
||||
<h1>Error reports (last 300)</h1>
|
||||
|
||||
<a class="{% if version = 3 %}strong{% endif %}" href="?version=3">[BACKEND ERRORS]</a>
|
||||
<a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="horizontal-list">
|
||||
</nav>
|
||||
<main class="horizontal-list">
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li>
|
||||
@@ -23,5 +25,5 @@ penpot - error list
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</main>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,7 +6,7 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div>[<a href="/dbg/error">⮜</a>]</div>
|
||||
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
|
||||
<div>[<a href="#head">head</a>]</div>
|
||||
<div>[<a href="#props">props</a>]</div>
|
||||
<div>[<a href="#context">context</a>]</div>
|
||||
|
||||
46
backend/resources/app/templates/error-report.v4.tmpl
Normal file
46
backend/resources/app/templates/error-report.v4.tmpl
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "app/templates/base.tmpl" %}
|
||||
|
||||
{% block title %}
|
||||
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
|
||||
<div>[<a href="#head">head</a>]</div>
|
||||
<!-- <div>[<a href="#props">props</a>]</div> -->
|
||||
<div>[<a href="#context">context</a>]</div>
|
||||
{% if report %}
|
||||
<div>[<a href="#report">report</a>]</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<main>
|
||||
<div class="table">
|
||||
<div class="table-row multiline">
|
||||
<div id="head" class="table-key">HEAD</div>
|
||||
<div class="table-val">
|
||||
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
|
||||
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
|
||||
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-row multiline">
|
||||
<div id="context" class="table-key">CONTEXT: </div>
|
||||
|
||||
<div class="table-val">
|
||||
<pre>{{context}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if report %}
|
||||
<div class="table-row multiline">
|
||||
<div id="report" class="table-key">REPORT:</div>
|
||||
<div class="table-val">
|
||||
<pre>{{report}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
* {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ small {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.strong {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.not-important {
|
||||
color: #888;
|
||||
font-weight: 200;
|
||||
@@ -57,14 +61,26 @@ nav {
|
||||
|
||||
nav > .title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav > .title > a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav > .title > a.strong {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
nav > .title > h1 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
font-size: 11px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav > .title > * {
|
||||
padding: 0px 6px;
|
||||
}
|
||||
|
||||
nav > div {
|
||||
|
||||
@@ -232,13 +232,22 @@
|
||||
(-> (io/resource "app/templates/error-report.v3.tmpl")
|
||||
(tmpl/render (-> content
|
||||
(assoc :id id)
|
||||
(assoc :version 3)
|
||||
(assoc :created-at (ct/format-inst created-at :rfc1123))))))
|
||||
|
||||
(render-template-v4 [{:keys [content id created-at]}]
|
||||
(-> (io/resource "app/templates/error-report.v4.tmpl")
|
||||
(tmpl/render (-> content
|
||||
(assoc :id id)
|
||||
(assoc :version 4)
|
||||
(assoc :created-at (ct/format-inst created-at :rfc1123))))))]
|
||||
|
||||
(if-let [report (get-report request)]
|
||||
(let [result (case (:version report)
|
||||
1 (render-template-v1 report)
|
||||
2 (render-template-v2 report)
|
||||
3 (render-template-v3 report))]
|
||||
3 (render-template-v3 report)
|
||||
4 (render-template-v4 report))]
|
||||
{::yres/status 200
|
||||
::yres/body result
|
||||
::yres/headers {"content-type" "text/html; charset=utf-8"
|
||||
@@ -246,20 +255,22 @@
|
||||
{::yres/status 404
|
||||
::yres/body "not found"})))
|
||||
|
||||
(def sql:error-reports
|
||||
(def ^:private sql:error-reports
|
||||
"SELECT id, created_at,
|
||||
content->>'~:hint' AS hint
|
||||
FROM server_error_report
|
||||
WHERE version = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 200")
|
||||
LIMIT 300")
|
||||
|
||||
(defn error-list-handler
|
||||
[{:keys [::db/pool]} _request]
|
||||
(let [items (->> (db/exec! pool [sql:error-reports])
|
||||
(defn- error-list-handler
|
||||
[{:keys [::db/pool]} {:keys [params]}]
|
||||
(let [version (or (some-> (get params :version) parse-long) 3)
|
||||
items (->> (db/exec! pool [sql:error-reports version])
|
||||
(map #(update % :created-at ct/format-inst :rfc1123)))]
|
||||
{::yres/status 200
|
||||
::yres/body (-> (io/resource "app/templates/error-list.tmpl")
|
||||
(tmpl/render {:items items}))
|
||||
(tmpl/render {:items items :version version}))
|
||||
::yres/headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}}))
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
(assoc :request/ip-addr (inet/parse-request request))
|
||||
(assoc :request/profile-id (get claims :uid))
|
||||
(assoc :request/auth-data auth)
|
||||
(assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
|
||||
(assoc :frontend/version (or (yreq/get-header request "x-frontend-version") "unknown")))))
|
||||
|
||||
(defmulti handle-error
|
||||
(fn [cause _ _]
|
||||
|
||||
@@ -113,6 +113,8 @@
|
||||
;; COLLECTOR API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare ^:private prepare-context-from-request)
|
||||
|
||||
;; Defines a service that collects the audit/activity log using
|
||||
;; internal database. Later this audit log can be transferred to
|
||||
;; an external storage and data cleared.
|
||||
@@ -126,6 +128,8 @@
|
||||
[::props {:optional true} [:map-of :keyword :any]]
|
||||
[::context {:optional true} [:map-of :keyword :any]]
|
||||
[::tracked-at {:optional true} ::ct/inst]
|
||||
[::created-at {:optional true} ::ct/inst]
|
||||
[::source {:optional true} ::sm/text]
|
||||
[::webhooks/event? {:optional true} ::sm/boolean]
|
||||
[::webhooks/batch-timeout {:optional true} ::ct/duration]
|
||||
[::webhooks/batch-key {:optional true}
|
||||
@@ -134,36 +138,8 @@
|
||||
(def ^:private check-event
|
||||
(sm/check-fn schema:event))
|
||||
|
||||
(defn- prepare-context-from-request
|
||||
[request]
|
||||
(let [client-event-origin (get-client-event-origin request)
|
||||
client-version (get-client-version request)
|
||||
client-user-agent (get-client-user-agent request)
|
||||
session-id (get-external-session-id request)
|
||||
key-id (::http/auth-key-id request)
|
||||
token-id (::actoken/id request)
|
||||
token-type (::actoken/type request)]
|
||||
(d/without-nils
|
||||
{:external-session-id session-id
|
||||
:initiator (or key-id "app")
|
||||
:access-token-id (some-> token-id str)
|
||||
:access-token-type (some-> token-type str)
|
||||
:client-event-origin client-event-origin
|
||||
:client-user-agent client-user-agent
|
||||
:client-version client-version
|
||||
:version (:full cf/version)})))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context (some-> params meta ::http/request prepare-context-from-request)
|
||||
event {::type "action"
|
||||
::profile-id (or (::rpc/profile-id params) uuid/zero)
|
||||
::ip-addr (::rpc/ip-addr params)}]
|
||||
(cond-> event
|
||||
(some? context)
|
||||
(assoc ::context context))))
|
||||
(def valid-event?
|
||||
(sm/validator schema:event))
|
||||
|
||||
(defn prepare-event
|
||||
[cfg mdata params result]
|
||||
@@ -212,6 +188,38 @@
|
||||
(::webhooks/event? resultm)
|
||||
false)}))
|
||||
|
||||
(defn- prepare-context-from-request
|
||||
"Prepare backend event context from request"
|
||||
[request]
|
||||
(let [client-event-origin (get-client-event-origin request)
|
||||
client-version (get-client-version request)
|
||||
client-user-agent (get-client-user-agent request)
|
||||
session-id (get-external-session-id request)
|
||||
key-id (::http/auth-key-id request)
|
||||
token-id (::actoken/id request)
|
||||
token-type (::actoken/type request)]
|
||||
(d/without-nils
|
||||
{:external-session-id session-id
|
||||
:initiator (or key-id "app")
|
||||
:access-token-id (some-> token-id str)
|
||||
:access-token-type (some-> token-type str)
|
||||
:client-event-origin client-event-origin
|
||||
:client-user-agent client-user-agent
|
||||
:client-version client-version
|
||||
:version (:full cf/version)})))
|
||||
|
||||
(defn event-from-rpc-params
|
||||
"Create a base event skeleton with pre-filled some important
|
||||
data that can be extracted from RPC params object"
|
||||
[params]
|
||||
(let [context (some-> params meta ::http/request prepare-context-from-request)
|
||||
event {::type "action"
|
||||
::profile-id (or (::rpc/profile-id params) uuid/zero)
|
||||
::ip-addr (::rpc/ip-addr params)}]
|
||||
(cond-> event
|
||||
(some? context)
|
||||
(assoc ::context context))))
|
||||
|
||||
(defn- event->params
|
||||
[event]
|
||||
(let [params {:id (uuid/next)
|
||||
@@ -238,8 +246,10 @@
|
||||
|
||||
(defn- handle-event!
|
||||
[cfg event]
|
||||
(let [params (event->params event)
|
||||
tnow (ct/now)]
|
||||
(let [tnow (ct/now)
|
||||
params (-> (event->params event)
|
||||
(assoc :created-at tnow)
|
||||
(update :tracked-at #(or % tnow)))]
|
||||
|
||||
(when (contains? cf/flags :audit-log-logger)
|
||||
(l/log! ::l/logger "app.audit"
|
||||
@@ -255,10 +265,7 @@
|
||||
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||
;; because of the timestamp precission (two concurrent requests), in
|
||||
;; this case we just retry the operation.
|
||||
(let [params (-> params
|
||||
(assoc :created-at tnow)
|
||||
(update :tracked-at #(or % tnow)))]
|
||||
(append-audit-entry cfg params)))
|
||||
(append-audit-entry cfg params))
|
||||
|
||||
(when (and (or (contains? cf/flags :telemetry)
|
||||
(cf/get :telemetry-enabled))
|
||||
@@ -269,8 +276,6 @@
|
||||
;;
|
||||
;; NOTE: this is only executed when general audit log is disabled
|
||||
(let [params (-> params
|
||||
(assoc :created-at tnow)
|
||||
(update :tracked-at #(or % tnow))
|
||||
(assoc :props {})
|
||||
(assoc :context {}))]
|
||||
(append-audit-entry cfg params)))
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
@@ -28,28 +29,30 @@
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- persist-on-database!
|
||||
[pool id report]
|
||||
[pool id version report]
|
||||
(when-not (db/read-only? pool)
|
||||
(db/insert! pool :server-error-report
|
||||
{:id id
|
||||
:version 3
|
||||
:version version
|
||||
:content (db/tjson report)})))
|
||||
|
||||
(defn- concurrent-exception?
|
||||
[cause]
|
||||
(or (instance? java.util.concurrent.CompletionException cause)
|
||||
(instance? java.util.concurrent.ExecutionException cause)))
|
||||
|
||||
(defn record->report
|
||||
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
|
||||
(assert (l/valid-record? record) "expectd valid log record")
|
||||
(if (or (instance? java.util.concurrent.CompletionException cause)
|
||||
(instance? java.util.concurrent.ExecutionException cause))
|
||||
(-> record
|
||||
(assoc ::trace (ex/format-throwable cause :data? true :explain? false :header? false :summary? false))
|
||||
(assoc ::l/cause (ex-cause cause))
|
||||
(record->report))
|
||||
(let [data (if (concurrent-exception? cause)
|
||||
(ex-data (ex-cause cause))
|
||||
(ex-data cause))
|
||||
|
||||
(let [data (ex-data cause)
|
||||
ctx (-> context
|
||||
(assoc :tenant (cf/get :tenant))
|
||||
(assoc :host (cf/get :host))
|
||||
(assoc :public-uri (str (cf/get :public-uri)))
|
||||
(assoc :service/tenant (cf/get :tenant))
|
||||
(assoc :service/host (cf/get :host))
|
||||
(assoc :service/public-uri (str (cf/get :public-uri)))
|
||||
(assoc :backend/version (:full cf/version))
|
||||
(assoc :logger/name logger)
|
||||
(assoc :logger/level level)
|
||||
(dissoc :request/params :value :params :data))]
|
||||
@@ -76,21 +79,58 @@
|
||||
{:data (pp/pprint-str data :length 30 :level 13)})
|
||||
|
||||
(when-let [explain (ex/explain data :length 30 :level 13)]
|
||||
{:explain explain})))))
|
||||
{:explain explain}))))
|
||||
|
||||
(defn error-record?
|
||||
[{:keys [::l/level]}]
|
||||
(= :error level))
|
||||
|
||||
(defn- handle-event
|
||||
(defn- handle-log-record
|
||||
"Convert the log record into a report object and persist it on the database"
|
||||
[{:keys [::db/pool]} {:keys [::l/id] :as record}]
|
||||
(try
|
||||
(let [uri (cf/get :public-uri)
|
||||
report (-> record record->report d/without-nils)]
|
||||
(l/debug :hint "registering error on database" :id id
|
||||
(l/dbg :hint "registering error on database"
|
||||
:id id
|
||||
:src "logging"
|
||||
:uri (str uri "/dbg/error/" id))
|
||||
(persist-on-database! pool id 3 report))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||
|
||||
(persist-on-database! pool id report))
|
||||
(defn- event->report
|
||||
[{:keys [::audit/context ::audit/props ::audit/ip-addr] :as record}]
|
||||
(let [context
|
||||
(reduce-kv (fn [context k v]
|
||||
(let [k' (keyword "frontend" (name k))]
|
||||
(-> context
|
||||
(dissoc k)
|
||||
(assoc k' v))))
|
||||
context
|
||||
context)
|
||||
|
||||
context
|
||||
(-> context
|
||||
(assoc :backend/tenant (cf/get :tenant))
|
||||
(assoc :backend/host (cf/get :host))
|
||||
(assoc :backend/public-uri (str (cf/get :public-uri)))
|
||||
(assoc :backend/version (:full cf/version))
|
||||
(assoc :frontend/ip-addr ip-addr))]
|
||||
|
||||
{:context (-> (into (sorted-map) context)
|
||||
(pp/pprint-str :length 50))
|
||||
:props (pp/pprint-str props :length 50)
|
||||
:hint (get props :hint)
|
||||
:report (get props :report)}))
|
||||
|
||||
(defn- handle-audit-event
|
||||
"Convert the log record into a report object and persist it on the database"
|
||||
[{:keys [::db/pool]} {:keys [::audit/id] :as event}]
|
||||
(try
|
||||
(let [uri (cf/get :public-uri)
|
||||
report (-> event event->report d/without-nils)]
|
||||
(l/dbg :hint "registering error on database"
|
||||
:id id
|
||||
:src "audit-log"
|
||||
:uri (str uri "/dbg/error/" id))
|
||||
(persist-on-database! pool id 4 report))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
|
||||
|
||||
@@ -100,26 +140,49 @@
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ cfg]
|
||||
(let [input (sp/chan :buf (sp/sliding-buffer 64)
|
||||
:xf (filter error-record?))]
|
||||
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||
|
||||
(px/thread {:name "penpot/database-reporter"}
|
||||
(let [input (sp/chan :buf (sp/sliding-buffer 256))
|
||||
thread (px/thread
|
||||
{:name "penpot/reporter/database"}
|
||||
(l/info :hint "initializing database error persistence")
|
||||
(try
|
||||
(loop []
|
||||
(when-let [record (sp/take! input)]
|
||||
(handle-event cfg record)
|
||||
(when-let [item (sp/take! input)]
|
||||
(cond
|
||||
(::l/id item)
|
||||
(handle-log-record cfg item)
|
||||
|
||||
(::audit/id item)
|
||||
(handle-audit-event cfg item)
|
||||
|
||||
:else
|
||||
(l/warn :hint "received unexpected item" :item item))
|
||||
|
||||
(recur)))
|
||||
|
||||
(catch InterruptedException _
|
||||
(l/debug :hint "reporter interrupted"))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error" :cause cause))
|
||||
(finally
|
||||
(sp/close! input)
|
||||
(remove-watch l/log-record ::reporter)
|
||||
(l/info :hint "reporter terminated"))))))
|
||||
(l/info :hint "reporter terminated"))))]
|
||||
|
||||
(add-watch l/log-record ::reporter
|
||||
(fn [_ _ _ record]
|
||||
(when (= :error (::l/level record))
|
||||
(sp/put! input record))))
|
||||
|
||||
{::input input
|
||||
::thread thread}))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ thread]
|
||||
(some-> thread px/interrupt!))
|
||||
[_ {:keys [::input ::thread]}]
|
||||
(remove-watch l/log-record ::reporter)
|
||||
(sp/close! input)
|
||||
(px/interrupt! thread))
|
||||
|
||||
(defn emit
|
||||
"Emit an event/report into the database reporter"
|
||||
[cfg event]
|
||||
(when-let [{:keys [::input]} (get cfg ::reporter)]
|
||||
(sp/put! input event)))
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.http.client :as http]
|
||||
[app.loggers.database :as ldb]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.util.json :as json]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]
|
||||
@@ -20,24 +21,27 @@
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- send-mattermost-notification!
|
||||
[cfg {:keys [id public-uri] :as report}]
|
||||
[cfg {:keys [id] :as report}]
|
||||
|
||||
|
||||
(let [text (str "Exception: " public-uri "/dbg/error/" id " "
|
||||
(let [url (u/join (cf/get :public-uri) "/dbg/error/" id)
|
||||
|
||||
text (str "Exception: " url " "
|
||||
(when-let [pid (:profile-id report)]
|
||||
(str "(pid: #uuid-" pid ")"))
|
||||
"\n"
|
||||
"- host: #" (:host report) "\n"
|
||||
"- tenant: #" (:tenant report) "\n"
|
||||
"- logger: #" (:logger report) "\n"
|
||||
"- request-path: `" (:request-path report) "`\n"
|
||||
"- origin: #" (:origin report) "\n"
|
||||
"- href: `" (:href report) "`\n"
|
||||
"- frontend-version: `" (:frontend-version report) "`\n"
|
||||
"- backend-version: `" (:backend-version report) "`\n"
|
||||
"\n"
|
||||
"```\n"
|
||||
(when-let [trace (:trace report)]
|
||||
(str "```\n"
|
||||
"Trace:\n"
|
||||
(:trace report)
|
||||
"```")
|
||||
trace
|
||||
"```")))
|
||||
|
||||
resp (http/req! cfg
|
||||
{:uri (cf/get :error-report-webhook)
|
||||
@@ -50,28 +54,49 @@
|
||||
(l/warn :hint "error on sending data"
|
||||
:response (pr-str resp)))))
|
||||
|
||||
(defn record->report
|
||||
(defn- record->report
|
||||
[{:keys [::l/context ::l/id ::l/cause] :as record}]
|
||||
(assert (l/valid-record? record) "expectd valid log record")
|
||||
|
||||
(let [public-uri (cf/get :public-uri)]
|
||||
{:id id
|
||||
:origin "logging"
|
||||
:tenant (cf/get :tenant)
|
||||
:host (cf/get :host)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:backend-version (or (:version/backend context) (:full cf/version))
|
||||
:frontend-version (:version/frontend context)
|
||||
:backend-version (:full cf/version)
|
||||
:frontend-version (:frontend/version context)
|
||||
:profile-id (:request/profile-id context)
|
||||
:request-path (:request/path context)
|
||||
:logger (::l/logger record)
|
||||
:trace (ex/format-throwable cause :detail? false :header? false)})
|
||||
:href (-> public-uri
|
||||
(assoc :path (:request/path context))
|
||||
(str))
|
||||
:trace (ex/format-throwable cause :detail? false :header? false)}))
|
||||
|
||||
(defn handle-event
|
||||
(defn- audit-event->report
|
||||
[{:keys [::audit/context ::audit/props ::audit/id] :as event}]
|
||||
{:id id
|
||||
:origin "audit-log"
|
||||
:tenant (cf/get :tenant)
|
||||
:host (cf/get :host)
|
||||
:backend-version (:full cf/version)
|
||||
:frontend-version (:version context)
|
||||
:profile-id (:audit/profile-id event)
|
||||
:href (get props :href)})
|
||||
|
||||
(defn- handle-log-record
|
||||
[cfg record]
|
||||
(when @enabled
|
||||
(try
|
||||
(let [report (record->report record)]
|
||||
(send-mattermost-notification! cfg report))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unhandled error" :cause cause)))))
|
||||
(l/warn :hint "unhandled error" :cause cause))))
|
||||
|
||||
(defn- handle-audit-event
|
||||
[cfg record]
|
||||
(try
|
||||
(let [report (audit-event->report record)]
|
||||
(send-mattermost-notification! cfg report))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unhandled error" :cause cause))))
|
||||
|
||||
(defmethod ig/assert-key ::reporter
|
||||
[_ params]
|
||||
@@ -80,27 +105,49 @@
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ cfg]
|
||||
(when-let [uri (cf/get :error-report-webhook)]
|
||||
(px/thread
|
||||
{:name "penpot/mattermost-reporter"
|
||||
:virtual true}
|
||||
(let [input (sp/chan :buf (sp/sliding-buffer 256))
|
||||
thread (px/thread
|
||||
{:name "penpot/reporter/mattermost"}
|
||||
(l/info :hint "initializing error reporter" :uri uri)
|
||||
(let [input (sp/chan :buf (sp/sliding-buffer 128)
|
||||
:xf (filter ldb/error-record?))]
|
||||
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||
|
||||
(try
|
||||
(loop []
|
||||
(when-let [msg (sp/take! input)]
|
||||
(handle-event cfg msg)
|
||||
(when-let [item (sp/take! input)]
|
||||
(when @enabled
|
||||
(cond
|
||||
(::l/id item)
|
||||
(handle-log-record cfg item)
|
||||
|
||||
(::audit/id item)
|
||||
(handle-audit-event cfg item)
|
||||
|
||||
:else
|
||||
(l/warn :hint "received unexpected item" :item item)))
|
||||
|
||||
(recur)))
|
||||
(catch InterruptedException _
|
||||
(l/debug :hint "reporter interrupted"))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected error" :cause cause))
|
||||
(finally
|
||||
(sp/close! input)
|
||||
(remove-watch l/log-record ::reporter)
|
||||
(l/info :hint "reporter terminated")))))))
|
||||
(l/info :hint "reporter terminated"))))]
|
||||
|
||||
(add-watch l/log-record ::reporter
|
||||
(fn [_ _ _ record]
|
||||
(when (= :error (::l/level record))
|
||||
(sp/put! input record))))
|
||||
|
||||
{::input input
|
||||
::thread thread})))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ thread]
|
||||
[_ {:keys [::input ::thread]}]
|
||||
(remove-watch l/log-record ::reporter)
|
||||
(some-> input sp/close!)
|
||||
(some-> thread px/interrupt!))
|
||||
|
||||
(defn emit
|
||||
"Emit an event/report into the mattermost reporter"
|
||||
[cfg event]
|
||||
(when-let [{:keys [::input]} (get cfg ::reporter)]
|
||||
(sp/put! input event)))
|
||||
|
||||
@@ -337,7 +337,13 @@
|
||||
::setup/props (ig/ref ::setup/props)
|
||||
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
::email/whitelist (ig/ref ::email/whitelist)
|
||||
|
||||
:app.loggers.database/reporter
|
||||
(ig/ref :app.loggers.database/reporter)
|
||||
|
||||
:app.loggers.mattermost/reporter
|
||||
(ig/ref :app.loggers.mattermost/reporter)}
|
||||
|
||||
:app.nitrate/client
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
|
||||
@@ -456,7 +456,10 @@
|
||||
:fn (mg/resource "app/migrations/sql/0142-add-sso-provider-table.sql")}
|
||||
|
||||
{:name "0143-http-session-v2-table"
|
||||
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}])
|
||||
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}
|
||||
|
||||
{:name "0144-mod-server-error-report-table"
|
||||
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE server_error_report DROP CONSTRAINT server_error_report_pkey;
|
||||
ALTER TABLE server_error_report ADD PRIMARY KEY (id);
|
||||
CREATE INDEX server_error_report__version__idx
|
||||
ON server_error_report ( version );
|
||||
@@ -16,6 +16,8 @@
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.database :as loggers.db]
|
||||
[app.loggers.mattermost :as loggers.mm]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.climit :as-alias climit]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@@ -36,52 +38,79 @@
|
||||
:context])
|
||||
|
||||
(defn- event->row [event]
|
||||
[(uuid/next)
|
||||
(:name event)
|
||||
(:source event)
|
||||
(:type event)
|
||||
(:timestamp event)
|
||||
(:created-at event)
|
||||
(:profile-id event)
|
||||
(db/inet (:ip-addr event))
|
||||
(db/tjson (:props event))
|
||||
(db/tjson (d/without-nils (:context event)))])
|
||||
[(::audit/id event)
|
||||
(::audit/name event)
|
||||
(::audit/source event)
|
||||
(::audit/type event)
|
||||
(::audit/tracked-at event)
|
||||
(::audit/created-at event)
|
||||
(::audit/profile-id event)
|
||||
(db/inet (::audit/ip-addr event))
|
||||
(db/tjson (::audit/props event))
|
||||
(db/tjson (d/without-nils (::audit/context event)))])
|
||||
|
||||
(defn- adjust-timestamp
|
||||
[{:keys [timestamp created-at] :as event}]
|
||||
(let [margin (inst-ms (ct/diff timestamp created-at))]
|
||||
[{:keys [::audit/tracked-at ::audit/created-at] :as event}]
|
||||
(let [margin (inst-ms (ct/diff tracked-at created-at))]
|
||||
(if (or (neg? margin)
|
||||
(> margin 3600000))
|
||||
;; If event is in future or lags more than 1 hour, we reasign
|
||||
;; timestamp to the server creation date
|
||||
;; tracked-at to the server creation date
|
||||
(-> event
|
||||
(assoc :timestamp created-at)
|
||||
(update :context assoc :original-timestamp timestamp))
|
||||
(assoc ::audit/tracked-at created-at)
|
||||
(update ::audit/context assoc :original-tracked-at tracked-at))
|
||||
event)))
|
||||
|
||||
(defn- handle-events
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
|
||||
(defn- exception-event?
|
||||
[{:keys [::audit/type ::audit/name] :as ev}]
|
||||
(and (= "action" type)
|
||||
(or (= "unhandled-exception" name)
|
||||
(= "exception-page" name))))
|
||||
|
||||
(def ^:private xf:map-event-row
|
||||
(comp
|
||||
(map adjust-timestamp)
|
||||
(map event->row)))
|
||||
|
||||
(defn- get-events
|
||||
[{:keys [::rpc/request-at ::rpc/profile-id events] :as params}]
|
||||
(let [request (-> params meta ::http/request)
|
||||
ip-addr (inet/parse-request request)
|
||||
tnow (ct/now)
|
||||
xform (comp
|
||||
(map (fn [event]
|
||||
(-> event
|
||||
(assoc :created-at tnow)
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :ip-addr ip-addr)
|
||||
(assoc :source "frontend"))))
|
||||
(filter :profile-id)
|
||||
(map adjust-timestamp)
|
||||
(map event->row))
|
||||
events (sequence xform events)]
|
||||
(when (seq events)
|
||||
(db/insert-many! pool :audit-log event-columns events))))
|
||||
|
||||
(def valid-event-types
|
||||
xform (map (fn [event]
|
||||
{::audit/id (uuid/next)
|
||||
::audit/type (:type event)
|
||||
::audit/name (:name event)
|
||||
::audit/props (:props event)
|
||||
::audit/context (:context event)
|
||||
::audit/profile-id profile-id
|
||||
::audit/ip-addr ip-addr
|
||||
::audit/source "frontend"
|
||||
::audit/tracked-at (:timestamp event)
|
||||
::audit/created-at request-at}))]
|
||||
|
||||
(sequence xform events)))
|
||||
|
||||
(defn- handle-events
|
||||
[{:keys [::db/pool] :as cfg} params]
|
||||
(let [events (get-events params)]
|
||||
|
||||
;; Look for error reports and save them on internal reports table
|
||||
(when-let [events (->> events
|
||||
(sequence (filter exception-event?))
|
||||
(not-empty))]
|
||||
(run! (partial loggers.db/emit cfg) events)
|
||||
(run! (partial loggers.mm/emit cfg) events))
|
||||
|
||||
;; Process and save events
|
||||
(when (seq events)
|
||||
(let [rows (sequence xf:map-event-row events)]
|
||||
(db/insert-many! pool :audit-log event-columns rows)))))
|
||||
|
||||
(def ^:private valid-event-types
|
||||
#{"action" "identify" "trigger"})
|
||||
|
||||
(def schema:event
|
||||
(def ^:private schema:frontend-event
|
||||
[:map {:title "Event"}
|
||||
[:name
|
||||
[:and {:gen/elements ["update-file", "get-profile"]}
|
||||
@@ -93,12 +122,13 @@
|
||||
[::sm/one-of {:format "string"} valid-event-types]]]
|
||||
[:props
|
||||
[:map-of :keyword ::sm/any]]
|
||||
[:timestamp ::ct/inst]
|
||||
[:context {:optional true}
|
||||
[:map-of :keyword ::sm/any]]])
|
||||
|
||||
(def schema:push-audit-events
|
||||
(def ^:private schema:push-audit-events
|
||||
[:map {:title "push-audit-events"}
|
||||
[:events [:vector schema:event]]])
|
||||
[:events [:vector schema:frontend-event]]])
|
||||
|
||||
(sv/defmethod ::push-audit-events
|
||||
{::climit/id :submit-audit-events/by-profile
|
||||
|
||||
@@ -103,6 +103,14 @@
|
||||
(assoc-in [::db/pool ::db/username] (:database-username config))
|
||||
(assoc-in [::db/pool ::db/password] (:database-password config))
|
||||
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
|
||||
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
|
||||
(update :app.rpc/methods
|
||||
(fn [state]
|
||||
(-> state
|
||||
(assoc :app.setup/templates templates)
|
||||
(assoc :app.loggers.mattermost/reporter nil)
|
||||
(assoc :app.loggers.database/reporter nil))))
|
||||
|
||||
(dissoc :app.srepl/server
|
||||
:app.http/server
|
||||
:app.http/route
|
||||
|
||||
@@ -239,22 +239,63 @@
|
||||
(recur cause))))))]
|
||||
|
||||
(with-out-str
|
||||
(print-all cause)))))
|
||||
(print-all cause))))
|
||||
|
||||
:cljs
|
||||
(defn format-throwable
|
||||
[cause & {:as opts}]
|
||||
(with-out-str
|
||||
(when-let [exdata (ex-data cause)]
|
||||
(when-let [hint (get exdata :hint)]
|
||||
(when (str/index-of hint "\n")
|
||||
(println "Hint:")
|
||||
(println "--------------------")
|
||||
(println hint)
|
||||
(println)))
|
||||
|
||||
(when-let [explain (get exdata ::sm/explain)]
|
||||
(println "Explain:")
|
||||
(println "--------------------")
|
||||
|
||||
(println (sm/humanize-explain explain))
|
||||
(println))
|
||||
|
||||
(when-let [explain (get exdata :explain)]
|
||||
(println "Server Explain:")
|
||||
(println "--------------------")
|
||||
(println explain))
|
||||
|
||||
(println "Data:")
|
||||
(println "--------------------")
|
||||
(pp/pprint (dissoc exdata ::sm/explain :explain))
|
||||
(println))
|
||||
|
||||
(when-let [trace (.-stack cause)]
|
||||
(println "Trace:")
|
||||
(println "--------------------")
|
||||
(println (.-stack cause))))))
|
||||
|
||||
(defn first-line
|
||||
[s]
|
||||
(let [break-index (str/index-of s "\n")]
|
||||
(if (pos? break-index)
|
||||
(subs s 0 break-index)
|
||||
s)))
|
||||
|
||||
(defn print-throwable
|
||||
[cause & {:as opts}]
|
||||
#?(:clj
|
||||
(println (format-throwable cause opts))
|
||||
:cljs
|
||||
(let [prefix (get opts :prefix "exception")
|
||||
title (str prefix ": " (ex-message cause))
|
||||
exdata (ex-data cause)]
|
||||
(let [prefix (get opts :prefix)
|
||||
data (ex-data cause)
|
||||
title (cond->> (or (some-> (:hint data) first-line)
|
||||
(ex-message cause))
|
||||
(string? prefix)
|
||||
(str prefix ": "))]
|
||||
|
||||
(js/console.group title)
|
||||
(when-let [explain (get exdata ::sm/explain)]
|
||||
(println (sm/humanize-explain explain)))
|
||||
|
||||
(js/console.log "\nData:")
|
||||
(pp/pprint (dissoc exdata ::sm/explain))
|
||||
|
||||
(js/console.log "\nTrace:")
|
||||
(js/console.error (.-stack cause)))))
|
||||
(try
|
||||
(js/console.log (format-throwable cause))
|
||||
(finally
|
||||
(js/console.groupEnd))))))
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.auth :as da]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.workspace :as-alias dw]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.main.worker]
|
||||
[app.util.globals :as glob]
|
||||
[app.util.globals :as g]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]
|
||||
@@ -32,44 +33,6 @@
|
||||
;; Will contain last uncaught exception
|
||||
(def last-exception nil)
|
||||
|
||||
(defn- print-data!
|
||||
[data]
|
||||
(-> data
|
||||
(dissoc ::sm/explain)
|
||||
(dissoc :explain)
|
||||
(dissoc ::trace)
|
||||
(dissoc ::instance)
|
||||
(pp/pprint {:width 70})))
|
||||
|
||||
(defn- print-explain!
|
||||
[data]
|
||||
(when-let [{:keys [errors] :as explain} (::sm/explain data)]
|
||||
(let [errors (mapv #(update % :schema sm/form) errors)]
|
||||
(pp/pprint errors {:width 100 :level 15 :length 20})))
|
||||
|
||||
(when-let [explain (:explain data)]
|
||||
(js/console.log explain)))
|
||||
|
||||
(defn- print-trace!
|
||||
[data]
|
||||
(some-> data ::trace js/console.log))
|
||||
|
||||
(defn- print-group!
|
||||
[message f]
|
||||
(try
|
||||
(js/console.group message)
|
||||
(f)
|
||||
(catch :default _ nil)
|
||||
(finally
|
||||
(js/console.groupEnd message))))
|
||||
|
||||
(defn print-cause!
|
||||
[message cause]
|
||||
(print-group! message (fn []
|
||||
(print-data! cause)
|
||||
(print-explain! cause)
|
||||
(print-trace! cause))))
|
||||
|
||||
(defn exception->error-data
|
||||
[cause]
|
||||
(let [data (ex-data cause)]
|
||||
@@ -78,18 +41,6 @@
|
||||
(assoc ::instance cause)
|
||||
(assoc ::trace (.-stack cause)))))
|
||||
|
||||
(defn print-error!
|
||||
[cause]
|
||||
(cond
|
||||
(map? cause)
|
||||
(print-cause! (:hint cause "Unexpected Error") cause)
|
||||
|
||||
(ex/error? cause)
|
||||
(print-cause! (ex-message cause) (ex-data cause))
|
||||
|
||||
:else
|
||||
(print-cause! (ex-message cause) (exception->error-data cause))))
|
||||
|
||||
(defn on-error
|
||||
"A general purpose error handler."
|
||||
[error]
|
||||
@@ -104,13 +55,66 @@
|
||||
;; Set the main potok error handler
|
||||
(reset! st/on-error on-error)
|
||||
|
||||
(defn generate-report
|
||||
[cause]
|
||||
(try
|
||||
(let [team-id (:current-team-id @st/state)
|
||||
file-id (:current-file-id @st/state)
|
||||
profile-id (:profile-id @st/state)
|
||||
data (ex-data cause)]
|
||||
|
||||
(with-out-str
|
||||
(println "Context:")
|
||||
(println "--------------------")
|
||||
(println "Hint: " (or (:hint data) (ex-message cause) "--"))
|
||||
(println "Prof ID: " (str (or profile-id "--")))
|
||||
(println "Team ID: " (str (or team-id "--")))
|
||||
(when-let [file-id (or (:file-id data) file-id)]
|
||||
(println "File ID: " (str file-id)))
|
||||
(println "Version: " (:full cf/version))
|
||||
(println "URI: " (str cf/public-uri))
|
||||
(println "HREF: " (rt/get-current-href))
|
||||
(println)
|
||||
|
||||
(println
|
||||
(ex/format-throwable cause))
|
||||
(println)
|
||||
|
||||
(println "Last events:")
|
||||
(println "--------------------")
|
||||
(pp/pprint @st/last-events {:length 200})
|
||||
(println)))
|
||||
(catch :default cause
|
||||
(.error js/console "error on generating report" cause)
|
||||
nil)))
|
||||
|
||||
(defn- show-not-blocking-error
|
||||
"Show a non user blocking error notification"
|
||||
[cause]
|
||||
(let [data (ex-data cause)
|
||||
hint (or (some-> (:hint data) ex/first-line)
|
||||
(ex-message cause))]
|
||||
|
||||
(st/emit!
|
||||
(ev/event {::ev/name "unhandled-exception"
|
||||
:hint hint
|
||||
:href (rt/get-current-href)
|
||||
:type (get data :type :unknown)
|
||||
:report (generate-report cause)})
|
||||
|
||||
(ntf/show {:content (tr "errors.unexpected-exception" hint)
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 3000}))))
|
||||
|
||||
(defmethod ptk/handle-error :default
|
||||
[error]
|
||||
(st/async-emit! (rt/assign-exception error))
|
||||
(print-group! "Unhandled Error"
|
||||
(fn []
|
||||
(print-trace! error)
|
||||
(print-data! error))))
|
||||
(if (and (string? (:hint error))
|
||||
(str/starts-with? (:hint error) "Assert failed:"))
|
||||
(ptk/handle-error (assoc error :type :assertion))
|
||||
(when-let [cause (::instance error)]
|
||||
(ex/print-throwable cause :prefix "Unexpected Error")
|
||||
(show-not-blocking-error cause))))
|
||||
|
||||
;; We receive a explicit authentication error; If the uri is for
|
||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||
@@ -142,10 +146,10 @@
|
||||
|
||||
(defmethod ptk/handle-error :validation
|
||||
[{:keys [code] :as error}]
|
||||
(print-group! "Validation Error"
|
||||
(fn []
|
||||
(print-data! error)
|
||||
(print-explain! error)))
|
||||
|
||||
(when-let [instance (get error ::instance)]
|
||||
(ex/print-throwable instance :prefix "Validation Error"))
|
||||
|
||||
(cond
|
||||
(= code :invalid-paste-data)
|
||||
(let [message (tr "errors.paste-data-validation")]
|
||||
@@ -193,23 +197,14 @@
|
||||
:else
|
||||
(st/async-emit! (rt/assign-exception error))))
|
||||
|
||||
|
||||
;; This is a pure frontend error that can be caused by an active
|
||||
;; assertion (assertion that is preserved on production builds). From
|
||||
;; the user perspective this should be treated as internal error.
|
||||
(defmethod ptk/handle-error :assertion
|
||||
[error]
|
||||
(ts/schedule
|
||||
#(st/emit! (ntf/show {:content (tr "errors.internal-assertion-error")
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 3000})))
|
||||
|
||||
(print-group! "Internal Assertion Error"
|
||||
(fn []
|
||||
(print-trace! error)
|
||||
(print-data! error)
|
||||
(print-explain! error))))
|
||||
(when-let [cause (::instance error)]
|
||||
(show-not-blocking-error cause)
|
||||
(ex/print-throwable cause :prefix "Assertion Error")))
|
||||
|
||||
;; ;; All the errors that happens on worker are handled here.
|
||||
(defmethod ptk/handle-error :worker-error
|
||||
@@ -221,9 +216,8 @@
|
||||
:level :error
|
||||
:timeout 3000})))
|
||||
|
||||
(print-group! "Internal Worker Error"
|
||||
(fn []
|
||||
(print-data! error))))
|
||||
(some-> (::instance error)
|
||||
(ex/print-throwable :prefix "Web Worker Error")))
|
||||
|
||||
;; Error on parsing an SVG
|
||||
(defmethod ptk/handle-error :svg-parser
|
||||
@@ -252,11 +246,9 @@
|
||||
|
||||
(defmethod ptk/handle-error ::exceptional-state
|
||||
[error]
|
||||
(when-let [cause (::instance error)]
|
||||
(js/console.log (.-stack cause)))
|
||||
|
||||
(ts/schedule
|
||||
#(st/emit! (rt/assign-exception error))))
|
||||
(when-let [instance (get error ::instance)]
|
||||
(ex/print-throwable instance :prefix "Exceptional State"))
|
||||
(ts/schedule #(st/emit! (rt/assign-exception error))))
|
||||
|
||||
(defn- redirect-to-dashboard
|
||||
[]
|
||||
@@ -264,7 +256,7 @@
|
||||
project-id (:current-project-id @st/state)]
|
||||
(if (and project-id team-id)
|
||||
(st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id}))
|
||||
(set! (.-href glob/location) ""))))
|
||||
(set! (.-href g/location) ""))))
|
||||
|
||||
(defmethod ptk/handle-error :restriction
|
||||
[{:keys [code] :as error}]
|
||||
@@ -312,9 +304,10 @@
|
||||
:text (tr "errors.deprecated.contact.text")
|
||||
:after (tr "errors.deprecated.contact.after")
|
||||
:on-click #(st/emit! (rt/nav :settings-feedback))}}))
|
||||
|
||||
:else
|
||||
(print-cause! "Restriction Error" error)))
|
||||
(when-let [cause (::instance error)]
|
||||
(ex/print-throwable cause :prefix "Restriction Error")
|
||||
(show-not-blocking-error cause))))
|
||||
|
||||
;; This happens when the backed server fails to process the
|
||||
;; request. This can be caused by an internal assertion or any other
|
||||
@@ -322,24 +315,9 @@
|
||||
|
||||
(defmethod ptk/handle-error :server-error
|
||||
[error]
|
||||
(st/async-emit! (rt/assign-exception error))
|
||||
(print-group! "Server Error"
|
||||
(fn []
|
||||
(print-data! (dissoc error :data))
|
||||
|
||||
(when-let [werror (:data error)]
|
||||
(cond
|
||||
(= :assertion (:type werror))
|
||||
(print-group! "Assertion Error"
|
||||
(fn []
|
||||
(print-data! werror)
|
||||
(print-explain! werror)))
|
||||
|
||||
:else
|
||||
(print-group! "Unexpected"
|
||||
(fn []
|
||||
(print-data! werror)
|
||||
(print-explain! werror))))))))
|
||||
(when-let [instance (get error ::instance)]
|
||||
(ex/print-throwable instance :prefix "Server Error"))
|
||||
(st/async-emit! (rt/assign-exception error)))
|
||||
|
||||
(defonce uncaught-error-handler
|
||||
(letfn [(is-ignorable-exception? [cause]
|
||||
@@ -354,28 +332,19 @@
|
||||
(when-let [cause (unchecked-get event "error")]
|
||||
(set! last-exception cause)
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(ex/print-throwable cause :prefix "uncaught exception")
|
||||
(st/async-emit!
|
||||
(ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause))
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 3000})))))
|
||||
|
||||
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||
(ts/schedule #(show-not-blocking-error cause)))))
|
||||
|
||||
(on-unhandled-rejection [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "reason")]
|
||||
(set! last-exception cause)
|
||||
(ex/print-throwable cause :prefix "uncaught rejection")
|
||||
(st/async-emit!
|
||||
(ntf/show {:content (tr "errors.unexpected-exception" (ex-message cause))
|
||||
:type :toast
|
||||
:level :error
|
||||
:timeout 3000}))))]
|
||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||
(ts/schedule #(show-not-blocking-error cause))))]
|
||||
|
||||
(.addEventListener glob/window "error" on-unhandled-error)
|
||||
(.addEventListener glob/window "unhandledrejection" on-unhandled-rejection)
|
||||
(.addEventListener g/window "error" on-unhandled-error)
|
||||
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
||||
(fn []
|
||||
(.removeEventListener glob/window "error" on-unhandled-error)
|
||||
(.removeEventListener glob/window "unhandledrejection" on-unhandled-rejection))))
|
||||
(.removeEventListener g/window "error" on-unhandled-error)
|
||||
(.removeEventListener g/window "unhandledrejection" on-unhandled-rejection))))
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as log]
|
||||
[app.main.data.dashboard :as dd]
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader*]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
@@ -360,7 +360,7 @@
|
||||
on-error
|
||||
(fn [cause]
|
||||
(reset! status* :error)
|
||||
(errors/print-error! cause)
|
||||
(ex/print-throwable cause)
|
||||
(rx/of (modal/hide)
|
||||
(ntf/error (tr "dashboard.libraries-and-templates.import-error"))))
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; FIXME: this is a workaround until we export this class on beicon library
|
||||
@@ -447,21 +446,20 @@
|
||||
(rx/of default)
|
||||
(rx/throw cause)))))))
|
||||
|
||||
|
||||
(mf/defc exception-section*
|
||||
{::mf/private true}
|
||||
[{:keys [data route] :as props}]
|
||||
(let [type (get data :type)
|
||||
report (mf/with-memo [data]
|
||||
(generate-report data))
|
||||
(some-> data ::errors/instance errors/generate-report))
|
||||
props (mf/spread-props props {:report report})]
|
||||
|
||||
(mf/with-effect [data route report]
|
||||
(let [params (:query-params route)
|
||||
params (u/map->query-string params)]
|
||||
(st/emit! (ptk/data-event ::ev/event
|
||||
{::ev/name "exception-page"
|
||||
(st/emit! (ev/event {::ev/name "exception-page"
|
||||
:type (get data :type :unknown)
|
||||
:href (rt/get-current-href)
|
||||
:hint (get data :hint)
|
||||
:path (get route :path)
|
||||
:report report
|
||||
|
||||
@@ -391,7 +391,7 @@
|
||||
(group-by :code)
|
||||
(clj->js))
|
||||
(catch :default cause
|
||||
(errors/print-error! cause))))))
|
||||
(ex/print-throwable cause))))))
|
||||
|
||||
(defn ^:export validate-schema
|
||||
[]
|
||||
@@ -399,7 +399,7 @@
|
||||
(let [file (dsh/lookup-file @st/state)]
|
||||
(cfv/validate-file-schema! file))
|
||||
(catch :default cause
|
||||
(errors/print-error! cause))))
|
||||
(ex/print-throwable cause))))
|
||||
|
||||
(defn ^:export repair
|
||||
[reload?]
|
||||
@@ -431,7 +431,7 @@
|
||||
(when reload?
|
||||
(dom/reload-current-window)))
|
||||
(fn [cause]
|
||||
(errors/print-error! cause)))))))))
|
||||
(ex/print-throwable cause)))))))))
|
||||
|
||||
(defn ^:export fix-orphan-shapes
|
||||
[]
|
||||
|
||||
@@ -1467,7 +1467,7 @@ msgid "errors.generic"
|
||||
msgstr "Something wrong has happened."
|
||||
|
||||
msgid "errors.unexpected-exception"
|
||||
msgstr "Unexpected exception: %s"
|
||||
msgstr "Unexpected error: %s"
|
||||
|
||||
#: src/app/main/errors.cljs:200
|
||||
msgid "errors.internal-assertion-error"
|
||||
|
||||
Reference in New Issue
Block a user