-
-
Notifications
You must be signed in to change notification settings - Fork 28
/
core.clj
353 lines (325 loc) · 16.4 KB
/
core.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
(ns sentry-clj.core
"A thin wrapper around the official Java library for Sentry."
(:require
[clojure.string :refer [blank?]]
[clojure.walk :as walk])
(:import
[io.sentry Breadcrumb DateUtils EventProcessor Sentry SentryEvent SentryLevel SentryOptions Instrumenter]
[io.sentry.protocol Message Request SentryId User]
[java.util Date HashMap Map UUID]))
(set! *warn-on-reflection* true)
(defn ^:private keyword->level
"Converts a keyword into an event level."
[level]
(case level
:debug SentryLevel/DEBUG
:info SentryLevel/INFO
:warning SentryLevel/WARNING
:error SentryLevel/ERROR
:fatal SentryLevel/FATAL
SentryLevel/INFO))
(defn java-util-hashmappify-vals
"Converts an ordinary Clojure map into a Clojure map with nested map
values recursively translated into java.util.HashMap objects. Based
on walk/stringify-keys."
[m]
(let [f (fn [[k v]]
(let [k (if (keyword? k) (str (symbol k)) k)
v (if (keyword? v) (str (symbol v)) v)]
(if (map? v) [k (HashMap. ^Map v)] [k v])))]
(walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m)))
(defn ^:private map->breadcrumb
"Converts a map into a Breadcrumb."
^Breadcrumb
[{:keys [type level message category data timestamp]}]
(let [breadcrumb (if timestamp (Breadcrumb. ^Date timestamp) (Breadcrumb.))]
(when type
(.setType breadcrumb type))
(when level
(.setLevel breadcrumb (keyword->level level)))
(when message
(.setMessage breadcrumb message))
(when category
(.setCategory breadcrumb category))
(when data
(doseq [[k v] (java-util-hashmappify-vals data)]
(.setData breadcrumb k v)))
breadcrumb))
(defn ^:private map->user
"Converts a map into a User."
^User
[{:keys [email id username ip-address data]}]
(let [user (User.)]
(when email
(.setEmail user email))
(when id
(.setId user id))
(when username
(.setUsername user username))
(when ip-address
(.setIpAddress user ip-address))
(when data
(.setData user data))
user))
(defn ^:private map->request
"Converts a map into a Request."
^Request
[{:keys [url method query-string data cookies headers env other]}]
(let [request (Request.)]
(when url
(.setUrl request url))
(when method
(.setMethod request method))
(when query-string
(.setQueryString request query-string))
(when data
(.setData request (java-util-hashmappify-vals data)))
(when cookies
(.setCookies request (java-util-hashmappify-vals cookies)))
(when headers
(.setHeaders request (java-util-hashmappify-vals headers)))
(when env
(.setEnvs request (java-util-hashmappify-vals env)))
(when other
(.setOthers request (java-util-hashmappify-vals other)))
request))
(defn ^:private merge-all-ex-data
"Merges ex-data of all ex-info exceptions in the cause chain of exn into extra.
Each ex-data is added under a separate key so that they don't clobber each other."
[extra exn]
(loop [exn exn
num 0
extra extra]
(if exn
(let [data (ex-data exn)]
(recur (ex-cause exn)
(inc num)
(cond-> extra
data
(assoc (if (zero? num)
"ex-data"
(str "ex-data, cause " num ": " (ex-message exn)))
data))))
extra)))
(defn ^:private map->event
"Converts a map into an event."
^SentryEvent
[{:keys [event-id message level release environment user request logger platform dist
tags breadcrumbs server-name extra fingerprints throwable transaction]}]
(let [sentry-event (SentryEvent. (DateUtils/getCurrentDateTime))
updated-message (if (string? message) {:message message} message)]
(when event-id
(.setEventId sentry-event (SentryId. ^UUID event-id)))
(when-let [{:keys [formatted message params]} updated-message]
(.setMessage sentry-event (doto
(Message.)
(.setFormatted formatted)
(.setMessage message)
(.setParams params))))
(when level
(.setLevel sentry-event (keyword->level level)))
(when dist
(.setDist sentry-event dist))
(when release
(.setRelease sentry-event release))
(when environment
(.setEnvironment sentry-event environment))
(when user
(.setUser sentry-event (map->user user)))
(when request
(.setRequest sentry-event (map->request request)))
(when logger
(.setLogger sentry-event logger))
(when platform
(.setPlatform sentry-event platform))
(when transaction
(.setTransaction sentry-event transaction))
(doseq [[k v] tags]
(.setTag sentry-event (name k) (str v)))
(when (seq breadcrumbs)
(doseq [breadcrumb (mapv map->breadcrumb breadcrumbs)]
(.addBreadcrumb sentry-event ^Breadcrumb breadcrumb)))
(when server-name
(.setServerName sentry-event server-name))
(when-let [data (merge-all-ex-data extra throwable)]
(doseq [[k v] (java-util-hashmappify-vals data)]
(.setExtra sentry-event k v)))
(when throwable
(.setThrowable sentry-event throwable))
(when (seq fingerprints)
(.setFingerprints sentry-event fingerprints))
sentry-event))
(def ^:private sentry-defaults
{:environment "production"
:debug false ;; Java SDK default
:enable-uncaught-exception-handler true ;; Java SDK default
:trace-options-requests true ;; Java SDK default
:serialization-max-depth 5 ;; default to 5, adjust lower if a circular reference loop occurs.
:enabled true})
(defn ^:private sentry-options
^SentryOptions
([dsn] (sentry-options dsn {}))
([dsn config]
(let [{:keys [environment
debug
logger
diagnostic-level
release
dist
server-name
shutdown-timeout-millis
in-app-includes
in-app-excludes
ignored-exceptions-for-type
enable-uncaught-exception-handler
before-send-fn
before-breadcrumb-fn
serialization-max-depth
traces-sample-rate
traces-sample-fn
trace-options-requests
instrumenter
event-processors
enabled]} (merge sentry-defaults config)
sentry-options (SentryOptions.)]
(.setDsn sentry-options dsn)
(when environment
(.setEnvironment sentry-options environment))
;;
;; When serializing out an object, say a Throwable, sometimes it happens
;; that the serialization goes into a circular reference loop and just locks up
;;
;; Turning on `{:debug true}` when initializing Sentry should expose the issue on your logs
;;
;; If you experience this issue, try adjusting the maximum depth to a low
;; number, such as 2 and see if that works for you.
;;
(when serialization-max-depth
(.setMaxDepth sentry-options serialization-max-depth)) ;; defaults to 100 in the SDK, but we default it to 5.
(when release
(.setRelease sentry-options release))
(when dist
(.setDist sentry-options dist))
(when server-name
(.setServerName sentry-options ^String server-name))
(when shutdown-timeout-millis
(.setShutdownTimeoutMillis sentry-options shutdown-timeout-millis)) ;; already set to 2000ms in the SDK
(doseq [in-app-include in-app-includes]
(.addInAppInclude sentry-options in-app-include))
(doseq [in-app-exclude in-app-excludes]
(.addInAppExclude sentry-options in-app-exclude))
(doseq [ignored-exception-for-type ignored-exceptions-for-type]
(try
(let [clazz (Class/forName ignored-exception-for-type)]
(when (isa? clazz Throwable)
(.addIgnoredExceptionForType sentry-options ^Throwable clazz)))
(catch Exception _))) ; just ignore it.
(when before-send-fn
(.setBeforeSend sentry-options ^SentryEvent
(reify io.sentry.SentryOptions$BeforeSendCallback
(execute [_ event hint]
(before-send-fn event hint)))))
(when before-breadcrumb-fn
(.setBeforeBreadcrumb sentry-options ^Breadcrumb
(reify io.sentry.SentryOptions$BeforeBreadcrumbCallback
(execute [_ breadcrumb hint]
(before-breadcrumb-fn breadcrumb hint)))))
(when traces-sample-rate
(.setTracesSampleRate sentry-options traces-sample-rate))
(when traces-sample-fn
(.setTracesSampler sentry-options ^io.sentry.SentryOptions$TracesSamplerCallback
(reify io.sentry.SentryOptions$TracesSamplerCallback
(sample [_ ctx]
(traces-sample-fn {:custom-sample-context (-> ctx
.getCustomSamplingContext
.getData)
:transaction-context (.getTransactionContext ctx)})))))
(when-let [instrumenter (case instrumenter
:sentry Instrumenter/SENTRY
:otel Instrumenter/OTEL
nil)]
(.setInstrumenter sentry-options ^Instrumenter instrumenter))
(doseq [event-processor event-processors]
(.addEventProcessor sentry-options ^EventProcessor event-processor))
(.setDebug sentry-options debug)
(.setLogger sentry-options logger)
(.setDiagnosticLevel sentry-options (keyword->level diagnostic-level))
(.setTraceOptionsRequests sentry-options trace-options-requests)
(.setEnableUncaughtExceptionHandler sentry-options enable-uncaught-exception-handler)
(.setEnabled sentry-options enabled)
sentry-options)))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn init!
"Initialize Sentry with the mandatory `dsn`
Other options include:
| key | description | default
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | -------
| `:environment` | Set the environment on which Sentry events will be logged, e.g., \"production\" | production
| `:enabled` | Enable or disable sentry. | true
| `:debug` | Enable SDK logging at the debug level | false
| `:logger` | Instance of `io.sentry.ILogger` (only applies when `:debug` is on) | `io.sentry.SystemOutLogger`
| `:diagnostic-level` | Log messages at or above this level (only applies when `:debug` is on) | `:debug`
| `:release` | All events are assigned to a particular release |
| `:dist` | Set the application distribution that will be sent with each event |
| `:server-name` | Set the server name that will be sent with each event |
| `:shutdown-timeout-millis` | Wait up to X milliseconds before shutdown if there are events to send | 2000ms
| `:in-app-includes` | A seqable collection (vector for example) containing package names to include when sending events |
| `:in-app-excludes` | A seqable collection (vector for example) containing package names to ignore when sending events |
| `:ignored-exceptions-for-type | Set exceptions that will be filtered out before sending to Sentry (a set of Classnames as Strings) |
| `:enable-uncaught-exception-handler` | Enables the uncaught exception handler | true
| `:before-send-fn` | A function (taking an event and a hint) |
| | The body of the function must not be lazy (i.e., don't use filter on its own!) and must return an event or nil |
| | If a nil is returned, the event will not be sent to Sentry |
| | [More Information](https://docs.sentry.io/platforms/java/data-management/sensitive-data/) |
| `:before-breadcrumb-fn` | A function (taking a breadcrumb and a hint) |
| | The body of the function must not be lazy (i.e., don't use filter on its own!) and must return a breadcrumb or nil |
| | If a nil is returned, the breadcrumb will not be sent to Sentry |
| | [More Information](https://docs.sentry.io/platforms/java/enriching-events/breadcrumbs/) |
| `:contexts` | A map of key/value pairs to attach to every Event that is sent. |
| | [More Information)(https://docs.sentry.io/platforms/java/enriching-events/context/) |
| `:traces-sample-rate` | Set a uniform sample rate(a number of between 0.0 and 1.0) for all transactions for tracing |
| `:traces-sample-fn` | A function (taking a custom sample context and a transaction context) enables you to control trace transactions |
| `:serialization-max-depth` | Set to a lower number, i.e., 2, if you experience circular reference errors when sending events | 5
| `:trace-options-request` | Set to enable or disable tracing of options requests | true
Some examples:
```clojure
(init! \"http://abcdefg@localhost:19000/2\")
```
```clojure
(init! \"http://abcdefg@localhost:19000/2\" {:environment \"production\" :debug true :release \"foo.bar@1.0.0\" :in-app-excludes [\"foo.bar\"])
```
```clojure
(init! \"http://abcdefg@localhost:19000/2\" {:before-send-fn (fn [event _] (when-not (= (.. event getMessage getMessage \"foo\")) event))})
```
```clojure
(init! \"http://abcdefg@localhost:19000/2\" {:before-send-fn (fn [event _] (.setServerName event \"fred\") event)})
```
```clojure
(init! \"http://abcdefg@localhost:19000/2\" {:contexts {:foo \"bar\" :baz \"wibble\"}})
```
"
([dsn] (init! dsn {}))
([dsn {:keys [contexts] :as config}]
{:pre [(not (blank? dsn))]}
(let [options (sentry-options dsn config)]
(Sentry/init ^SentryOptions options)
(when contexts
(Sentry/configureScope (reify io.sentry.ScopeCallback
(run [_ scope]
(doseq [[k v] (java-util-hashmappify-vals contexts)]
(.setContexts scope ^String k ^Object {"value" v})))))))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn close!
"Closes the SDK"
[]
(Sentry/close))
(defn send-event
"Sends the given event to Sentry, returning the event's id
Supports sending throwables:
```
(sentry/send-event {:message \"oh no\",
:throwable (RuntimeException. \"foo bar\"})
```
"
[event]
(str (Sentry/captureEvent (map->event event))))