findbug 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +45 -8
- data/app/models/findbug/error_event.rb +34 -6
- data/app/models/findbug/performance_event.rb +38 -16
- data/docs/docs.html +976 -0
- data/docs/index.html +594 -8
- data/lib/findbug/adapter_helper.rb +74 -0
- data/lib/findbug/version.rb +1 -1
- data/lib/findbug.rb +1 -0
- data/lib/generators/findbug/templates/create_findbug_error_events.rb +3 -3
- data/lib/generators/findbug/templates/create_findbug_performance_events.rb +3 -3
- metadata +8 -3
data/docs/docs.html
ADDED
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>FindBug Documentation</title>
|
|
7
|
+
<meta name="description" content="Complete documentation for FindBug — self-hosted error tracking and performance monitoring for Rails.">
|
|
8
|
+
<link rel="canonical" href="https://findbug.dev/docs">
|
|
9
|
+
|
|
10
|
+
<!-- Favicon -->
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fafafa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2l1.88 1.88'/%3E%3Cpath d='M14.12 3.88L16 2'/%3E%3Cpath d='M9 7.13v-1a3.003 3.003 0 1 1 6 0v1'/%3E%3Cpath d='M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6'/%3E%3Cpath d='M12 20v-9'/%3E%3Cpath d='M6.53 9C4.6 8.8 3 7.1 3 5'/%3E%3Cpath d='M6 13H2'/%3E%3Cpath d='M3 21c0-2.1 1.7-3.9 3.8-4'/%3E%3Cpath d='M20.97 5c0 2.1-1.6 3.8-3.5 4'/%3E%3Cpath d='M22 13h-4'/%3E%3Cpath d='M17.2 17c2.1.1 3.8 1.9 3.8 4'/%3E%3C/svg%3E">
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
--background: 0 0% 3.9%;
|
|
16
|
+
--foreground: 0 0% 98%;
|
|
17
|
+
--card: 0 0% 6%;
|
|
18
|
+
--muted: 0 0% 14.9%;
|
|
19
|
+
--muted-foreground: 0 0% 63.9%;
|
|
20
|
+
--border: 0 0% 14.9%;
|
|
21
|
+
--primary: 0 0% 98%;
|
|
22
|
+
--primary-foreground: 0 0% 9%;
|
|
23
|
+
--accent: 0 0% 14.9%;
|
|
24
|
+
--success: 142.1 76.2% 36.3%;
|
|
25
|
+
--info: 217.2 91.2% 59.8%;
|
|
26
|
+
--warning: 38 92% 50%;
|
|
27
|
+
--error: 0 84.2% 60.2%;
|
|
28
|
+
--radius: 0.5rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
32
|
+
html { scroll-behavior: smooth; }
|
|
33
|
+
body {
|
|
34
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
|
35
|
+
background-color: hsl(var(--background));
|
|
36
|
+
color: hsl(var(--foreground));
|
|
37
|
+
line-height: 1.65;
|
|
38
|
+
min-height: 100vh;
|
|
39
|
+
-webkit-font-smoothing: antialiased;
|
|
40
|
+
}
|
|
41
|
+
a { color: inherit; text-decoration: none; transition: color 0.15s; }
|
|
42
|
+
|
|
43
|
+
/* ------- Navigation (same as index.html) ------- */
|
|
44
|
+
.site-nav {
|
|
45
|
+
position: fixed;
|
|
46
|
+
top: 0;
|
|
47
|
+
left: 0;
|
|
48
|
+
right: 0;
|
|
49
|
+
z-index: 50;
|
|
50
|
+
background-color: hsl(var(--background) / 0.95);
|
|
51
|
+
backdrop-filter: blur(10px);
|
|
52
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
53
|
+
}
|
|
54
|
+
.nav-container {
|
|
55
|
+
max-width: 1400px;
|
|
56
|
+
margin: 0 auto;
|
|
57
|
+
padding: 0 1.5rem;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
height: 3.5rem;
|
|
61
|
+
gap: 1.5rem;
|
|
62
|
+
}
|
|
63
|
+
.logo {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 0.5rem;
|
|
67
|
+
font-size: 0.875rem;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
}
|
|
70
|
+
.logo svg { width: 1.25rem; height: 1.25rem; }
|
|
71
|
+
.nav-links {
|
|
72
|
+
display: flex;
|
|
73
|
+
gap: 1.25rem;
|
|
74
|
+
flex: 1;
|
|
75
|
+
font-size: 0.875rem;
|
|
76
|
+
}
|
|
77
|
+
.nav-links a { color: hsl(var(--muted-foreground)); }
|
|
78
|
+
.nav-links a:hover { color: hsl(var(--foreground)); }
|
|
79
|
+
.nav-links a.active { color: hsl(var(--foreground)); }
|
|
80
|
+
.nav-right { display: flex; gap: 0.5rem; align-items: center; }
|
|
81
|
+
.btn {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 0.5rem;
|
|
85
|
+
padding: 0.5rem 1rem;
|
|
86
|
+
border-radius: var(--radius);
|
|
87
|
+
font-size: 0.8125rem;
|
|
88
|
+
font-weight: 500;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: all 0.15s;
|
|
91
|
+
border: 1px solid transparent;
|
|
92
|
+
}
|
|
93
|
+
.btn-sm { padding: 0.375rem 0.75rem; }
|
|
94
|
+
.btn-primary {
|
|
95
|
+
background-color: hsl(var(--primary));
|
|
96
|
+
color: hsl(var(--primary-foreground));
|
|
97
|
+
}
|
|
98
|
+
.btn-primary:hover { background-color: hsl(var(--primary) / 0.9); }
|
|
99
|
+
.btn-outline {
|
|
100
|
+
border-color: hsl(var(--border));
|
|
101
|
+
background-color: transparent;
|
|
102
|
+
}
|
|
103
|
+
.btn-outline:hover { background-color: hsl(var(--accent)); }
|
|
104
|
+
|
|
105
|
+
/* ------- Docs Layout ------- */
|
|
106
|
+
.docs-shell {
|
|
107
|
+
max-width: 1400px;
|
|
108
|
+
margin: 0 auto;
|
|
109
|
+
padding: 5rem 1.5rem 4rem;
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: 240px 1fr;
|
|
112
|
+
gap: 3rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Sidebar */
|
|
116
|
+
.docs-sidebar {
|
|
117
|
+
position: sticky;
|
|
118
|
+
top: 4.5rem;
|
|
119
|
+
align-self: start;
|
|
120
|
+
max-height: calc(100vh - 5rem);
|
|
121
|
+
overflow-y: auto;
|
|
122
|
+
padding-right: 0.5rem;
|
|
123
|
+
}
|
|
124
|
+
.docs-sidebar h4 {
|
|
125
|
+
font-size: 0.6875rem;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
text-transform: uppercase;
|
|
128
|
+
letter-spacing: 0.06em;
|
|
129
|
+
color: hsl(var(--muted-foreground));
|
|
130
|
+
margin: 1.25rem 0 0.5rem;
|
|
131
|
+
}
|
|
132
|
+
.docs-sidebar h4:first-child { margin-top: 0; }
|
|
133
|
+
.docs-sidebar ul { list-style: none; }
|
|
134
|
+
.docs-sidebar a {
|
|
135
|
+
display: block;
|
|
136
|
+
padding: 0.3rem 0.5rem;
|
|
137
|
+
font-size: 0.8125rem;
|
|
138
|
+
color: hsl(var(--muted-foreground));
|
|
139
|
+
border-radius: 0.375rem;
|
|
140
|
+
border-left: 2px solid transparent;
|
|
141
|
+
}
|
|
142
|
+
.docs-sidebar a:hover {
|
|
143
|
+
color: hsl(var(--foreground));
|
|
144
|
+
background-color: hsl(var(--muted) / 0.5);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Main content */
|
|
148
|
+
.docs-main { min-width: 0; }
|
|
149
|
+
|
|
150
|
+
.docs-main h1 {
|
|
151
|
+
font-size: 2.25rem;
|
|
152
|
+
font-weight: 700;
|
|
153
|
+
letter-spacing: -0.02em;
|
|
154
|
+
margin-bottom: 0.5rem;
|
|
155
|
+
}
|
|
156
|
+
.docs-main .lead {
|
|
157
|
+
font-size: 1.125rem;
|
|
158
|
+
color: hsl(var(--muted-foreground));
|
|
159
|
+
margin-bottom: 2.5rem;
|
|
160
|
+
}
|
|
161
|
+
.docs-main h2 {
|
|
162
|
+
font-size: 1.75rem;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
letter-spacing: -0.01em;
|
|
165
|
+
margin-top: 3rem;
|
|
166
|
+
margin-bottom: 0.75rem;
|
|
167
|
+
padding-bottom: 0.5rem;
|
|
168
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
169
|
+
scroll-margin-top: 4.5rem;
|
|
170
|
+
}
|
|
171
|
+
.docs-main h3 {
|
|
172
|
+
font-size: 1.25rem;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
margin-top: 2rem;
|
|
175
|
+
margin-bottom: 0.5rem;
|
|
176
|
+
scroll-margin-top: 4.5rem;
|
|
177
|
+
}
|
|
178
|
+
.docs-main h4 {
|
|
179
|
+
font-size: 1rem;
|
|
180
|
+
font-weight: 600;
|
|
181
|
+
margin-top: 1.25rem;
|
|
182
|
+
margin-bottom: 0.5rem;
|
|
183
|
+
color: hsl(var(--foreground) / 0.9);
|
|
184
|
+
}
|
|
185
|
+
.docs-main p {
|
|
186
|
+
margin-bottom: 1rem;
|
|
187
|
+
color: hsl(var(--foreground) / 0.92);
|
|
188
|
+
}
|
|
189
|
+
.docs-main ul, .docs-main ol {
|
|
190
|
+
margin: 0 0 1rem 1.5rem;
|
|
191
|
+
color: hsl(var(--foreground) / 0.92);
|
|
192
|
+
}
|
|
193
|
+
.docs-main li { margin-bottom: 0.35rem; }
|
|
194
|
+
|
|
195
|
+
/* Inline code */
|
|
196
|
+
.docs-main code {
|
|
197
|
+
font-family: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
|
198
|
+
background-color: hsl(var(--muted) / 0.6);
|
|
199
|
+
padding: 0.125rem 0.375rem;
|
|
200
|
+
border-radius: 0.25rem;
|
|
201
|
+
font-size: 0.85em;
|
|
202
|
+
color: hsl(var(--foreground));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Code blocks */
|
|
206
|
+
pre {
|
|
207
|
+
background-color: hsl(var(--card));
|
|
208
|
+
border: 1px solid hsl(var(--border));
|
|
209
|
+
border-radius: var(--radius);
|
|
210
|
+
padding: 1rem 1.25rem;
|
|
211
|
+
overflow-x: auto;
|
|
212
|
+
margin: 0.75rem 0 1.5rem;
|
|
213
|
+
font-size: 0.8125rem;
|
|
214
|
+
line-height: 1.6;
|
|
215
|
+
}
|
|
216
|
+
.docs-main pre code {
|
|
217
|
+
background: transparent;
|
|
218
|
+
padding: 0;
|
|
219
|
+
border-radius: 0;
|
|
220
|
+
color: hsl(var(--foreground));
|
|
221
|
+
font-size: inherit;
|
|
222
|
+
}
|
|
223
|
+
.kw { color: #ff7b72; } /* keyword - red/coral */
|
|
224
|
+
.str { color: #a5d6ff; } /* string - blue */
|
|
225
|
+
.sym { color: #79c0ff; } /* symbol - light blue */
|
|
226
|
+
.com { color: hsl(var(--muted-foreground)); font-style: italic; }
|
|
227
|
+
.num { color: #d2a8ff; } /* number/literal - purple */
|
|
228
|
+
.var { color: #ffa657; } /* class - orange */
|
|
229
|
+
|
|
230
|
+
/* Callouts */
|
|
231
|
+
.callout {
|
|
232
|
+
border-left: 3px solid hsl(var(--info));
|
|
233
|
+
background-color: hsl(var(--info) / 0.08);
|
|
234
|
+
padding: 0.875rem 1rem;
|
|
235
|
+
border-radius: 0.375rem;
|
|
236
|
+
margin: 1rem 0 1.5rem;
|
|
237
|
+
font-size: 0.9rem;
|
|
238
|
+
}
|
|
239
|
+
.callout.warn {
|
|
240
|
+
border-left-color: hsl(var(--warning));
|
|
241
|
+
background-color: hsl(var(--warning) / 0.08);
|
|
242
|
+
}
|
|
243
|
+
.callout.success {
|
|
244
|
+
border-left-color: hsl(var(--success));
|
|
245
|
+
background-color: hsl(var(--success) / 0.08);
|
|
246
|
+
}
|
|
247
|
+
.callout strong { display: block; margin-bottom: 0.25rem; }
|
|
248
|
+
.callout p:last-child { margin-bottom: 0; }
|
|
249
|
+
|
|
250
|
+
/* Tables */
|
|
251
|
+
.docs-main table {
|
|
252
|
+
width: 100%;
|
|
253
|
+
border-collapse: collapse;
|
|
254
|
+
margin: 0.5rem 0 1.5rem;
|
|
255
|
+
font-size: 0.875rem;
|
|
256
|
+
}
|
|
257
|
+
.docs-main th, .docs-main td {
|
|
258
|
+
text-align: left;
|
|
259
|
+
padding: 0.625rem 0.75rem;
|
|
260
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
261
|
+
}
|
|
262
|
+
.docs-main th {
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
color: hsl(var(--muted-foreground));
|
|
265
|
+
background-color: hsl(var(--muted) / 0.3);
|
|
266
|
+
font-size: 0.75rem;
|
|
267
|
+
text-transform: uppercase;
|
|
268
|
+
letter-spacing: 0.04em;
|
|
269
|
+
}
|
|
270
|
+
.docs-main td code { font-size: 0.8125rem; }
|
|
271
|
+
|
|
272
|
+
/* Footer */
|
|
273
|
+
.docs-footer {
|
|
274
|
+
margin-top: 4rem;
|
|
275
|
+
padding-top: 2rem;
|
|
276
|
+
border-top: 1px solid hsl(var(--border));
|
|
277
|
+
color: hsl(var(--muted-foreground));
|
|
278
|
+
font-size: 0.875rem;
|
|
279
|
+
display: flex;
|
|
280
|
+
justify-content: space-between;
|
|
281
|
+
flex-wrap: wrap;
|
|
282
|
+
gap: 1rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Responsive */
|
|
286
|
+
@media (max-width: 900px) {
|
|
287
|
+
.docs-shell {
|
|
288
|
+
grid-template-columns: 1fr;
|
|
289
|
+
padding: 5rem 1rem 3rem;
|
|
290
|
+
gap: 1.5rem;
|
|
291
|
+
}
|
|
292
|
+
.docs-sidebar {
|
|
293
|
+
position: static;
|
|
294
|
+
max-height: none;
|
|
295
|
+
border-bottom: 1px solid hsl(var(--border));
|
|
296
|
+
padding-bottom: 1.5rem;
|
|
297
|
+
}
|
|
298
|
+
.nav-links { display: none; }
|
|
299
|
+
}
|
|
300
|
+
</style>
|
|
301
|
+
</head>
|
|
302
|
+
<body>
|
|
303
|
+
|
|
304
|
+
<!-- Navigation -->
|
|
305
|
+
<nav class="site-nav">
|
|
306
|
+
<div class="nav-container">
|
|
307
|
+
<a href="/" class="logo">
|
|
308
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
309
|
+
<path d="M8 2l1.88 1.88"/><path d="M14.12 3.88L16 2"/>
|
|
310
|
+
<path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/>
|
|
311
|
+
<path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/>
|
|
312
|
+
<path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/>
|
|
313
|
+
<path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/>
|
|
314
|
+
<path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/>
|
|
315
|
+
<path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/>
|
|
316
|
+
</svg>
|
|
317
|
+
<span>FindBug</span>
|
|
318
|
+
</a>
|
|
319
|
+
<div class="nav-links">
|
|
320
|
+
<a href="/#features">Features</a>
|
|
321
|
+
<a href="/#comparison">Compare</a>
|
|
322
|
+
<a href="/#architecture">Architecture</a>
|
|
323
|
+
<a href="/#demo">Demo</a>
|
|
324
|
+
<a href="/docs" class="active">Docs</a>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="nav-right">
|
|
327
|
+
<a href="/#demo" class="btn btn-primary btn-sm">Dashboard Demo</a>
|
|
328
|
+
<a href="https://github.com/ITSSOUMIT/findbug" target="_blank" class="btn btn-outline btn-sm">
|
|
329
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
|
330
|
+
GitHub
|
|
331
|
+
</a>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</nav>
|
|
335
|
+
|
|
336
|
+
<div class="docs-shell">
|
|
337
|
+
|
|
338
|
+
<!-- Sidebar -->
|
|
339
|
+
<aside class="docs-sidebar">
|
|
340
|
+
<h4>Getting Started</h4>
|
|
341
|
+
<ul>
|
|
342
|
+
<li><a href="#introduction">Introduction</a></li>
|
|
343
|
+
<li><a href="#requirements">Requirements</a></li>
|
|
344
|
+
<li><a href="#installation">Installation</a></li>
|
|
345
|
+
<li><a href="#quick-start">Quick start</a></li>
|
|
346
|
+
</ul>
|
|
347
|
+
|
|
348
|
+
<h4>Configuration</h4>
|
|
349
|
+
<ul>
|
|
350
|
+
<li><a href="#configuration">Overview</a></li>
|
|
351
|
+
<li><a href="#config-core">Core settings</a></li>
|
|
352
|
+
<li><a href="#config-errors">Error capture</a></li>
|
|
353
|
+
<li><a href="#config-performance">Performance</a></li>
|
|
354
|
+
<li><a href="#config-scrubbing">Data scrubbing</a></li>
|
|
355
|
+
<li><a href="#config-storage">Storage & retention</a></li>
|
|
356
|
+
<li><a href="#config-dashboard">Dashboard</a></li>
|
|
357
|
+
<li><a href="#config-alerts">Alerts</a></li>
|
|
358
|
+
<li><a href="#config-misc">Release & logger</a></li>
|
|
359
|
+
</ul>
|
|
360
|
+
|
|
361
|
+
<h4>Usage</h4>
|
|
362
|
+
<ul>
|
|
363
|
+
<li><a href="#capture-auto">Automatic capture</a></li>
|
|
364
|
+
<li><a href="#capture-manual">Manual capture</a></li>
|
|
365
|
+
<li><a href="#controller-helpers">Controller helpers</a></li>
|
|
366
|
+
<li><a href="#breadcrumbs">Breadcrumbs</a></li>
|
|
367
|
+
<li><a href="#performance-tracking">Performance tracking</a></li>
|
|
368
|
+
</ul>
|
|
369
|
+
|
|
370
|
+
<h4>Operations</h4>
|
|
371
|
+
<ul>
|
|
372
|
+
<li><a href="#dashboard">Dashboard</a></li>
|
|
373
|
+
<li><a href="#alerts">Alerts</a></li>
|
|
374
|
+
<li><a href="#rake-tasks">Rake tasks</a></li>
|
|
375
|
+
<li><a href="#database-support">Database support</a></li>
|
|
376
|
+
<li><a href="#multi-tenant">Multi-tenant apps</a></li>
|
|
377
|
+
<li><a href="#activejob-mode">ActiveJob mode</a></li>
|
|
378
|
+
</ul>
|
|
379
|
+
|
|
380
|
+
<h4>Reference</h4>
|
|
381
|
+
<ul>
|
|
382
|
+
<li><a href="#api-reference">API reference</a></li>
|
|
383
|
+
<li><a href="#architecture">Architecture</a></li>
|
|
384
|
+
<li><a href="#testing">Testing</a></li>
|
|
385
|
+
<li><a href="#troubleshooting">Troubleshooting</a></li>
|
|
386
|
+
</ul>
|
|
387
|
+
</aside>
|
|
388
|
+
|
|
389
|
+
<!-- Main content -->
|
|
390
|
+
<main class="docs-main">
|
|
391
|
+
|
|
392
|
+
<h1>FindBug Documentation</h1>
|
|
393
|
+
<p class="lead">
|
|
394
|
+
Self-hosted error tracking and performance monitoring for Rails. Sentry-like
|
|
395
|
+
functionality, with every byte of data on your own infrastructure.
|
|
396
|
+
</p>
|
|
397
|
+
|
|
398
|
+
<!-- ============================================================
|
|
399
|
+
GETTING STARTED
|
|
400
|
+
============================================================ -->
|
|
401
|
+
|
|
402
|
+
<h2 id="introduction">Introduction</h2>
|
|
403
|
+
<p>
|
|
404
|
+
FindBug is a Rails engine that captures exceptions and performance data from your
|
|
405
|
+
application, stores them in Redis (as a high-speed buffer) and your existing relational
|
|
406
|
+
database, and surfaces them through a built-in web dashboard mounted at
|
|
407
|
+
<code>/findbug</code>. There are no external services, API keys, or per-seat fees.
|
|
408
|
+
</p>
|
|
409
|
+
<p>What you get out of the box:</p>
|
|
410
|
+
<ul>
|
|
411
|
+
<li>Automatic exception capture from controllers, middleware, and <code>Rails.error</code></li>
|
|
412
|
+
<li>Performance instrumentation for HTTP requests and SQL with automatic N+1 detection</li>
|
|
413
|
+
<li>A web dashboard for browsing and resolving errors</li>
|
|
414
|
+
<li>Alerting over Email, Slack, Discord, or custom webhooks</li>
|
|
415
|
+
<li>A built-in background thread that persists Redis events to your database every 30 seconds (no Sidekiq required)</li>
|
|
416
|
+
</ul>
|
|
417
|
+
|
|
418
|
+
<h2 id="requirements">Requirements</h2>
|
|
419
|
+
<ul>
|
|
420
|
+
<li>Ruby 3.1 or newer</li>
|
|
421
|
+
<li>Rails 7.0 or newer (7.x and 8.x both supported)</li>
|
|
422
|
+
<li>Redis 4.0 or newer</li>
|
|
423
|
+
<li>A relational database — PostgreSQL, MySQL, or SQLite (see <a href="#database-support">Database Support</a>)</li>
|
|
424
|
+
</ul>
|
|
425
|
+
|
|
426
|
+
<h2 id="installation">Installation</h2>
|
|
427
|
+
<p>Add the gem to your <code>Gemfile</code>:</p>
|
|
428
|
+
<pre><code><span class="kw">gem</span> <span class="str">"findbug"</span>, <span class="sym">"~> 0.5.0"</span></code></pre>
|
|
429
|
+
|
|
430
|
+
<p>Install and run the generator:</p>
|
|
431
|
+
<pre><code><span class="com">$</span> bundle install
|
|
432
|
+
<span class="com">$</span> rails generate findbug:install
|
|
433
|
+
<span class="com">$</span> rails db:migrate</code></pre>
|
|
434
|
+
|
|
435
|
+
<p>The generator creates:</p>
|
|
436
|
+
<ul>
|
|
437
|
+
<li><code>config/initializers/findbug.rb</code> — your configuration</li>
|
|
438
|
+
<li>Three migrations under <code>db/migrate/</code>:
|
|
439
|
+
<ul>
|
|
440
|
+
<li><code>create_findbug_error_events</code></li>
|
|
441
|
+
<li><code>create_findbug_performance_events</code></li>
|
|
442
|
+
<li><code>create_findbug_alert_channels</code></li>
|
|
443
|
+
</ul>
|
|
444
|
+
</li>
|
|
445
|
+
</ul>
|
|
446
|
+
|
|
447
|
+
<div class="callout success">
|
|
448
|
+
<strong>Database-agnostic migrations</strong>
|
|
449
|
+
<p>As of v0.5.0 the migrations detect your adapter at <code>db:migrate</code> time and pick the right column type — <code>jsonb</code> on PostgreSQL, <code>json</code> on MySQL, <code>text</code> on SQLite. No manual editing needed.</p>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<h2 id="quick-start">Quick start</h2>
|
|
453
|
+
<p>After running the migrations, set dashboard credentials:</p>
|
|
454
|
+
<pre>
|
|
455
|
+
<code><span class="kw">export</span> FINDBUG_USERNAME=admin
|
|
456
|
+
<span class="kw">export</span> FINDBUG_PASSWORD=your-secure-password</code>
|
|
457
|
+
</pre>
|
|
458
|
+
|
|
459
|
+
<p>Then start your app and visit <code>http://localhost:3000/findbug</code>.</p>
|
|
460
|
+
<p>FindBug is now:</p>
|
|
461
|
+
<ul>
|
|
462
|
+
<li>Capturing unhandled exceptions automatically</li>
|
|
463
|
+
<li>Monitoring 10% of requests (default <code>performance_sample_rate</code>)</li>
|
|
464
|
+
<li>Flushing the Redis buffer to your database every 30 seconds</li>
|
|
465
|
+
</ul>
|
|
466
|
+
|
|
467
|
+
<!-- ============================================================
|
|
468
|
+
CONFIGURATION
|
|
469
|
+
============================================================ -->
|
|
470
|
+
|
|
471
|
+
<h2 id="configuration">Configuration</h2>
|
|
472
|
+
<p>
|
|
473
|
+
All configuration lives in <code>config/initializers/findbug.rb</code>. Every setting has
|
|
474
|
+
a sensible default — you only need to override what's different in your environment.
|
|
475
|
+
</p>
|
|
476
|
+
|
|
477
|
+
<h3 id="config-core">Core settings</h3>
|
|
478
|
+
<table>
|
|
479
|
+
<thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead>
|
|
480
|
+
<tbody>
|
|
481
|
+
<tr><td><code>enabled</code></td><td><code>true</code></td><td>Turn FindBug on/off globally. Set to <code>false</code> in test envs.</td></tr>
|
|
482
|
+
<tr><td><code>redis_url</code></td><td><code>redis://localhost:6379/1</code></td><td>Redis URL for the buffer. We recommend a dedicated database number.</td></tr>
|
|
483
|
+
<tr><td><code>redis_pool_size</code></td><td><code>5</code></td><td>Size of the dedicated Redis connection pool.</td></tr>
|
|
484
|
+
<tr><td><code>redis_pool_timeout</code></td><td><code>1</code></td><td>Seconds to wait for a connection before giving up (fail-fast).</td></tr>
|
|
485
|
+
</tbody>
|
|
486
|
+
</table>
|
|
487
|
+
|
|
488
|
+
<h3 id="config-errors">Error capture</h3>
|
|
489
|
+
<table>
|
|
490
|
+
<thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead>
|
|
491
|
+
<tbody>
|
|
492
|
+
<tr><td><code>sample_rate</code></td><td><code>1.0</code></td><td>Fraction of errors to capture (0.0–1.0). Lower for very high-traffic apps.</td></tr>
|
|
493
|
+
<tr><td><code>ignored_exceptions</code></td><td><code>[]</code></td><td>Array of exception classes to ignore.</td></tr>
|
|
494
|
+
<tr><td><code>ignored_paths</code></td><td><code>[]</code></td><td>Array of regexes — requests matching these paths are skipped.</td></tr>
|
|
495
|
+
</tbody>
|
|
496
|
+
</table>
|
|
497
|
+
|
|
498
|
+
<pre><code><span class="var">Findbug</span>.configure <span class="kw">do</span> |config|
|
|
499
|
+
config.sample_rate = <span class="num">1.0</span>
|
|
500
|
+
config.ignored_exceptions = [
|
|
501
|
+
<span class="var">ActiveRecord</span>::<span class="var">RecordNotFound</span>,
|
|
502
|
+
<span class="var">ActionController</span>::<span class="var">RoutingError</span>
|
|
503
|
+
]
|
|
504
|
+
config.ignored_paths = [<span class="str">/^\/health/</span>, <span class="str">/^\/assets/</span>]
|
|
505
|
+
<span class="kw">end</span></code></pre>
|
|
506
|
+
|
|
507
|
+
<h3 id="config-performance">Performance monitoring</h3>
|
|
508
|
+
<table>
|
|
509
|
+
<thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead>
|
|
510
|
+
<tbody>
|
|
511
|
+
<tr><td><code>performance_enabled</code></td><td><code>true</code></td><td>Master switch for the performance instrumenter.</td></tr>
|
|
512
|
+
<tr><td><code>performance_sample_rate</code></td><td><code>0.1</code></td><td>Fraction of requests to instrument (10% is fine for most apps).</td></tr>
|
|
513
|
+
<tr><td><code>slow_request_threshold_ms</code></td><td><code>0</code></td><td>Only persist requests slower than this. <code>0</code> = all sampled requests.</td></tr>
|
|
514
|
+
<tr><td><code>slow_query_threshold_ms</code></td><td><code>100</code></td><td>SQL queries above this duration are flagged in the dashboard.</td></tr>
|
|
515
|
+
</tbody>
|
|
516
|
+
</table>
|
|
517
|
+
|
|
518
|
+
<h3 id="config-scrubbing">Data scrubbing (security)</h3>
|
|
519
|
+
<p>
|
|
520
|
+
FindBug filters sensitive request and context data before it touches Redis.
|
|
521
|
+
Field names matching <code>scrub_fields</code> are replaced with <code>[FILTERED]</code>.
|
|
522
|
+
</p>
|
|
523
|
+
<table>
|
|
524
|
+
<thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead>
|
|
525
|
+
<tbody>
|
|
526
|
+
<tr><td><code>scrub_fields</code></td><td>password, token, api_key, secret, credit_card, ssn, …</td><td>Field names to redact.</td></tr>
|
|
527
|
+
<tr><td><code>scrub_headers</code></td><td><code>true</code></td><td>Strips <code>Authorization</code>, <code>Cookie</code>, and similar headers.</td></tr>
|
|
528
|
+
<tr><td><code>scrub_header_names</code></td><td><code>[]</code></td><td>Additional header names to redact.</td></tr>
|
|
529
|
+
</tbody>
|
|
530
|
+
</table>
|
|
531
|
+
|
|
532
|
+
<h3 id="config-storage">Storage & retention</h3>
|
|
533
|
+
<table>
|
|
534
|
+
<thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead>
|
|
535
|
+
<tbody>
|
|
536
|
+
<tr><td><code>retention_days</code></td><td><code>30</code></td><td>Older records are removed by the cleanup job.</td></tr>
|
|
537
|
+
<tr><td><code>max_buffer_size</code></td><td><code>10_000</code></td><td>Hard cap on Redis buffer entries — protects Redis memory.</td></tr>
|
|
538
|
+
<tr><td><code>buffer_ttl</code></td><td><code>86_400</code></td><td>Redis key TTL in seconds (24h). Stale buffered events expire automatically.</td></tr>
|
|
539
|
+
<tr><td><code>persist_interval</code></td><td><code>30</code></td><td>How often (in seconds) the background thread flushes Redis to the DB.</td></tr>
|
|
540
|
+
<tr><td><code>persist_batch_size</code></td><td><code>100</code></td><td>How many events to batch-insert per flush.</td></tr>
|
|
541
|
+
<tr><td><code>auto_persist</code></td><td><code>true</code></td><td>Use the built-in thread. Set to <code>false</code> to drive persistence via ActiveJob.</td></tr>
|
|
542
|
+
<tr><td><code>queue_name</code></td><td><code>"findbug"</code></td><td>Queue name when running in ActiveJob mode.</td></tr>
|
|
543
|
+
</tbody>
|
|
544
|
+
</table>
|
|
545
|
+
|
|
546
|
+
<h3 id="config-dashboard">Dashboard</h3>
|
|
547
|
+
<table>
|
|
548
|
+
<thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead>
|
|
549
|
+
<tbody>
|
|
550
|
+
<tr><td><code>web_username</code></td><td><code>nil</code></td><td>HTTP basic-auth username. Dashboard is disabled unless both username and password are set.</td></tr>
|
|
551
|
+
<tr><td><code>web_password</code></td><td><code>nil</code></td><td>HTTP basic-auth password.</td></tr>
|
|
552
|
+
<tr><td><code>web_path</code></td><td><code>"/findbug"</code></td><td>Mount path of the engine.</td></tr>
|
|
553
|
+
</tbody>
|
|
554
|
+
</table>
|
|
555
|
+
|
|
556
|
+
<div class="callout warn">
|
|
557
|
+
<strong>Don't commit credentials</strong>
|
|
558
|
+
<p>Drive these from <code>ENV["FINDBUG_USERNAME"]</code> / <code>ENV["FINDBUG_PASSWORD"]</code>. The dashboard is automatically disabled if either is blank, so missing env vars in CI are safe.</p>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
<h3 id="config-alerts">Alerts</h3>
|
|
562
|
+
<p>
|
|
563
|
+
Alert channels (Email, Slack, Discord, Webhook) are created at runtime through the
|
|
564
|
+
dashboard at <code>/findbug/alerts</code> — no code changes required.
|
|
565
|
+
The only thing you configure in code is the throttle window:
|
|
566
|
+
</p>
|
|
567
|
+
<pre><code><span class="var">Findbug</span>.configure <span class="kw">do</span> |config|
|
|
568
|
+
config.alerts <span class="kw">do</span> |alerts|
|
|
569
|
+
alerts.throttle_period = <span class="num">5</span>.minutes
|
|
570
|
+
<span class="kw">end</span>
|
|
571
|
+
<span class="kw">end</span></code></pre>
|
|
572
|
+
<p>See the <a href="#alerts">Alerts</a> section for channel-by-channel details.</p>
|
|
573
|
+
|
|
574
|
+
<h3 id="config-misc">Release & logger</h3>
|
|
575
|
+
<table>
|
|
576
|
+
<thead><tr><th>Setting</th><th>Default</th><th>Description</th></tr></thead>
|
|
577
|
+
<tbody>
|
|
578
|
+
<tr><td><code>release</code></td><td><code>ENV["FINDBUG_RELEASE"]</code></td><td>A version identifier (e.g. git SHA). Useful for tracking which deploy introduced an error.</td></tr>
|
|
579
|
+
<tr><td><code>environment</code></td><td><code>Rails.env</code></td><td>Override the environment label.</td></tr>
|
|
580
|
+
<tr><td><code>logger</code></td><td><code>Rails.logger</code></td><td>Replace with your own logger if needed.</td></tr>
|
|
581
|
+
</tbody>
|
|
582
|
+
</table>
|
|
583
|
+
|
|
584
|
+
<!-- ============================================================
|
|
585
|
+
USAGE
|
|
586
|
+
============================================================ -->
|
|
587
|
+
|
|
588
|
+
<h2 id="capture-auto">Automatic error capture</h2>
|
|
589
|
+
<p>FindBug captures these out of the box:</p>
|
|
590
|
+
<ul>
|
|
591
|
+
<li>Unhandled exceptions raised in any controller action</li>
|
|
592
|
+
<li>Errors reported through <code>Rails.error.handle</code> / <code>Rails.error.report</code></li>
|
|
593
|
+
<li>Any exception that bubbles up through the Rack middleware stack</li>
|
|
594
|
+
</ul>
|
|
595
|
+
<p>You generally don't need to do anything to enable this — it's wired up by the Railtie when the engine boots.</p>
|
|
596
|
+
|
|
597
|
+
<h2 id="capture-manual">Manual error capture</h2>
|
|
598
|
+
|
|
599
|
+
<p>To capture an exception you've rescued, call <code>Findbug.capture_exception</code>:</p>
|
|
600
|
+
<pre><code><span class="kw">begin</span>
|
|
601
|
+
risky_operation
|
|
602
|
+
<span class="kw">rescue</span> => e
|
|
603
|
+
<span class="var">Findbug</span>.capture_exception(e, user_id: current_user.id)
|
|
604
|
+
<span class="kw">end</span></code></pre>
|
|
605
|
+
|
|
606
|
+
<p>For non-exception events (warnings, informational messages), use <code>capture_message</code>:</p>
|
|
607
|
+
<pre><code><span class="var">Findbug</span>.capture_message(<span class="str">"Rate limit exceeded"</span>, <span class="sym">:warning</span>, user_id: <span class="num">123</span>)</code></pre>
|
|
608
|
+
<p>Severity levels: <code>:info</code>, <code>:warning</code>, <code>:error</code>.</p>
|
|
609
|
+
|
|
610
|
+
<h2 id="controller-helpers">Controller helpers</h2>
|
|
611
|
+
<p>
|
|
612
|
+
The Railtie installs several helper methods on <code>ActionController::Base</code> and
|
|
613
|
+
<code>ActionController::API</code>. Use them to attach request-scoped context that's
|
|
614
|
+
included with every error captured during the request.
|
|
615
|
+
</p>
|
|
616
|
+
|
|
617
|
+
<h3>findbug_set_user(user)</h3>
|
|
618
|
+
<p>Attach the current user. <code>id</code>, <code>email</code>, and <code>username</code> (or <code>name</code>) are extracted via <code>try</code>.</p>
|
|
619
|
+
<pre><code><span class="kw">before_action</span> :set_findbug_context
|
|
620
|
+
|
|
621
|
+
<span class="kw">def</span> set_findbug_context
|
|
622
|
+
findbug_set_user(current_user)
|
|
623
|
+
<span class="kw">end</span></code></pre>
|
|
624
|
+
|
|
625
|
+
<h3>findbug_set_context(data)</h3>
|
|
626
|
+
<p>Deep-merge arbitrary key-value data into the current request's context.</p>
|
|
627
|
+
<pre><code>findbug_set_context(
|
|
628
|
+
plan: current_user&.plan,
|
|
629
|
+
organization_id: current_org&.id
|
|
630
|
+
)</code></pre>
|
|
631
|
+
|
|
632
|
+
<h3>findbug_tag(key, value)</h3>
|
|
633
|
+
<p>Add a single tag — short, filterable values that show up in the dashboard's filter bar.</p>
|
|
634
|
+
<pre><code>findbug_tag(<span class="sym">:region</span>, <span class="str">"us-east-1"</span>)</code></pre>
|
|
635
|
+
|
|
636
|
+
<h3>findbug_capture(exception, extra = {})</h3>
|
|
637
|
+
<p>Capture an exception with the current request context already merged in.</p>
|
|
638
|
+
<pre><code><span class="kw">rescue</span> <span class="var">ExternalAPIError</span> => e
|
|
639
|
+
findbug_capture(e, api: <span class="str">"payment_gateway"</span>)
|
|
640
|
+
<span class="com"># handle gracefully</span>
|
|
641
|
+
<span class="kw">end</span></code></pre>
|
|
642
|
+
|
|
643
|
+
<h2 id="breadcrumbs">Breadcrumbs</h2>
|
|
644
|
+
<p>
|
|
645
|
+
Breadcrumbs are a trail of small events leading up to an error. They're per-request
|
|
646
|
+
and cleared automatically after the response is sent.
|
|
647
|
+
</p>
|
|
648
|
+
<pre><code>findbug_breadcrumb(<span class="str">"User clicked checkout"</span>, category: <span class="str">"ui"</span>)
|
|
649
|
+
findbug_breadcrumb(<span class="str">"Payment API called"</span>, category: <span class="str">"http"</span>, data: { amount: <span class="num">99.99</span> })</code></pre>
|
|
650
|
+
<p>When an error fires later in the request, the breadcrumbs are attached to it. View them on the error detail page in the dashboard.</p>
|
|
651
|
+
|
|
652
|
+
<h2 id="performance-tracking">Performance tracking</h2>
|
|
653
|
+
<p>Automatic instrumentation captures, per request:</p>
|
|
654
|
+
<ul>
|
|
655
|
+
<li>Total duration</li>
|
|
656
|
+
<li>SQL query count and total DB time</li>
|
|
657
|
+
<li>Slow queries (above <code>slow_query_threshold_ms</code>)</li>
|
|
658
|
+
<li>N+1 patterns (detected automatically)</li>
|
|
659
|
+
<li>View rendering count and time</li>
|
|
660
|
+
</ul>
|
|
661
|
+
|
|
662
|
+
<p>To track a custom operation (e.g. an outbound API call), wrap it in <code>Findbug.track_performance</code>:</p>
|
|
663
|
+
<pre><code><span class="var">Findbug</span>.track_performance(<span class="str">"external_api_call"</span>) <span class="kw">do</span>
|
|
664
|
+
<span class="var">ExternalAPI</span>.fetch_data
|
|
665
|
+
<span class="kw">end</span></code></pre>
|
|
666
|
+
<p>Custom transactions get their own row in the <code>findbug_performance_events</code> table with <code>transaction_type = "custom"</code>.</p>
|
|
667
|
+
|
|
668
|
+
<!-- ============================================================
|
|
669
|
+
OPERATIONS
|
|
670
|
+
============================================================ -->
|
|
671
|
+
|
|
672
|
+
<h2 id="dashboard">Dashboard</h2>
|
|
673
|
+
<p>The dashboard lives at <code>config.web_path</code> (default <code>/findbug</code>). It exposes:</p>
|
|
674
|
+
<ul>
|
|
675
|
+
<li><strong>Overview</strong> — aggregate stats, the most recent unresolved errors, and a throughput chart.</li>
|
|
676
|
+
<li><strong>Errors</strong> — filterable list of all error events. Click through to see backtrace, request data, breadcrumbs, and user context.</li>
|
|
677
|
+
<li><strong>Performance</strong> — slowest transactions, N+1 hotspots, throughput over time.</li>
|
|
678
|
+
<li><strong>Alerts</strong> — manage alert channels (see below).</li>
|
|
679
|
+
</ul>
|
|
680
|
+
<p>Authentication is plain HTTP basic auth. The dashboard is automatically disabled if <code>web_username</code> or <code>web_password</code> is blank.</p>
|
|
681
|
+
|
|
682
|
+
<h2 id="alerts">Alerts</h2>
|
|
683
|
+
<p>FindBug ships with four built-in alert channels. Configure them at <code>/findbug/alerts</code>:</p>
|
|
684
|
+
|
|
685
|
+
<table>
|
|
686
|
+
<thead><tr><th>Channel</th><th>What you configure</th></tr></thead>
|
|
687
|
+
<tbody>
|
|
688
|
+
<tr><td><strong>Email</strong></td><td>Recipient addresses. Sends via your app's existing ActionMailer setup.</td></tr>
|
|
689
|
+
<tr><td><strong>Slack</strong></td><td>Incoming-webhook URL.</td></tr>
|
|
690
|
+
<tr><td><strong>Discord</strong></td><td>Webhook URL.</td></tr>
|
|
691
|
+
<tr><td><strong>Webhook</strong></td><td>Any HTTPS endpoint — FindBug POSTs JSON.</td></tr>
|
|
692
|
+
</tbody>
|
|
693
|
+
</table>
|
|
694
|
+
|
|
695
|
+
<p>
|
|
696
|
+
Channel configurations are stored encrypted in the <code>findbug_alert_channels</code> table,
|
|
697
|
+
so the alert UI uses Rails encryption — make sure you've set up
|
|
698
|
+
<code>active_record.encryption</code> in your Rails app (see
|
|
699
|
+
<a href="https://guides.rubyonrails.org/active_record_encryption.html" target="_blank">the Rails guide</a>).
|
|
700
|
+
</p>
|
|
701
|
+
|
|
702
|
+
<h3>Throttling</h3>
|
|
703
|
+
<p>
|
|
704
|
+
Same-error notifications are throttled by fingerprint. The default window is 5 minutes —
|
|
705
|
+
configurable via <code>config.alerts.throttle_period</code>.
|
|
706
|
+
</p>
|
|
707
|
+
|
|
708
|
+
<h3>Sending a test notification</h3>
|
|
709
|
+
<p>Each channel in the UI has a "Send test" button that fires a dummy payload to the channel — useful for verifying webhook URLs and email addresses before going live.</p>
|
|
710
|
+
|
|
711
|
+
<h2 id="rake-tasks">Rake tasks</h2>
|
|
712
|
+
<p>FindBug ships with several maintenance tasks. Run <code>rake -T findbug</code> for the live list.</p>
|
|
713
|
+
<table>
|
|
714
|
+
<thead><tr><th>Task</th><th>Description</th></tr></thead>
|
|
715
|
+
<tbody>
|
|
716
|
+
<tr><td><code>rails findbug:status</code></td><td>Show current configuration, Redis buffer lengths, and circuit-breaker state. Use this first when debugging.</td></tr>
|
|
717
|
+
<tr><td><code>rails findbug:test</code></td><td>Raise and capture a test exception. Verifies the capture pipeline end-to-end.</td></tr>
|
|
718
|
+
<tr><td><code>rails findbug:flush</code></td><td>Manually drain the Redis buffer to the database. Useful when shutting down or debugging.</td></tr>
|
|
719
|
+
<tr><td><code>rails findbug:cleanup</code></td><td>Run the retention cleanup immediately. Same job the background thread runs daily.</td></tr>
|
|
720
|
+
<tr><td><code>rails findbug:clear_buffers</code></td><td>Wipe the Redis buffer. <strong>Drops un-persisted events</strong> — use with care.</td></tr>
|
|
721
|
+
<tr><td><code>rails findbug:db:stats</code></td><td>Print row counts for the FindBug tables.</td></tr>
|
|
722
|
+
</tbody>
|
|
723
|
+
</table>
|
|
724
|
+
|
|
725
|
+
<h2 id="database-support">Database support</h2>
|
|
726
|
+
<p>
|
|
727
|
+
As of <strong>v0.5.0</strong> FindBug is database-agnostic. It introspects your
|
|
728
|
+
<code>ActiveRecord::Base.connection.adapter_name</code> at migration and query time and
|
|
729
|
+
picks the correct column types and SQL functions automatically.
|
|
730
|
+
</p>
|
|
731
|
+
<table>
|
|
732
|
+
<thead><tr><th>Adapter</th><th>JSON column type</th><th>Time bucketing SQL</th></tr></thead>
|
|
733
|
+
<tbody>
|
|
734
|
+
<tr><td>PostgreSQL / PostGIS</td><td><code>jsonb</code></td><td><code>date_trunc(...)</code></td></tr>
|
|
735
|
+
<tr><td>MySQL / Mysql2</td><td><code>json</code></td><td><code>DATE_FORMAT(...)</code> / <code>DATE(...)</code></td></tr>
|
|
736
|
+
<tr><td>SQLite</td><td><code>text</code> (with JSON serialisation in the model)</td><td><code>strftime(...)</code> / <code>DATE(...)</code></td></tr>
|
|
737
|
+
</tbody>
|
|
738
|
+
</table>
|
|
739
|
+
<p>
|
|
740
|
+
The model's JSON accessors normalise reads to native Ruby <code>Hash</code> / <code>Array</code>
|
|
741
|
+
regardless of the underlying column type — your code is identical across adapters.
|
|
742
|
+
</p>
|
|
743
|
+
|
|
744
|
+
<h2 id="multi-tenant">Multi-tenant applications</h2>
|
|
745
|
+
<p>
|
|
746
|
+
FindBug works with multi-tenant Rails apps, but the setup depends on
|
|
747
|
+
<em>how</em> your app isolates tenants. The matrix below shows what's possible per database adapter.
|
|
748
|
+
</p>
|
|
749
|
+
|
|
750
|
+
<table>
|
|
751
|
+
<thead><tr><th>Adapter</th><th>Tenancy model</th><th>FindBug support</th></tr></thead>
|
|
752
|
+
<tbody>
|
|
753
|
+
<tr>
|
|
754
|
+
<td><strong>PostgreSQL</strong></td>
|
|
755
|
+
<td>Schema-per-tenant (Apartment's default)</td>
|
|
756
|
+
<td>✅ First-class — keep FindBug tables in the <code>public</code> schema (instructions below).</td>
|
|
757
|
+
</tr>
|
|
758
|
+
<tr>
|
|
759
|
+
<td><strong>PostgreSQL · MySQL · SQLite</strong></td>
|
|
760
|
+
<td>Row-level (<code>tenant_id</code> column on each table)</td>
|
|
761
|
+
<td>✅ Nothing special — FindBug tables aren't tenant-scoped anyway.</td>
|
|
762
|
+
</tr>
|
|
763
|
+
<tr>
|
|
764
|
+
<td><strong>MySQL</strong></td>
|
|
765
|
+
<td>Database-per-tenant (Apartment with <code>use_schemas = false</code>)</td>
|
|
766
|
+
<td>⚠️ Doable but awkward — you'll need a shared database the app connection can reach for FindBug's tables, plus cross-database references. Not recommended unless tenant count is small.</td>
|
|
767
|
+
</tr>
|
|
768
|
+
<tr>
|
|
769
|
+
<td><strong>SQLite</strong></td>
|
|
770
|
+
<td>File-per-tenant</td>
|
|
771
|
+
<td>❌ Not practical — Apartment supports it nominally, but SQLite isn't typically chosen for multi-tenant production apps. Use row-level scoping instead.</td>
|
|
772
|
+
</tr>
|
|
773
|
+
</tbody>
|
|
774
|
+
</table>
|
|
775
|
+
|
|
776
|
+
<div class="callout">
|
|
777
|
+
<strong>TL;DR</strong>
|
|
778
|
+
<p>If you're on PostgreSQL with <a href="https://github.com/rails-on-services/apartment" target="_blank">ros-apartment</a>, follow the three steps below. On any other adapter — or with row-level multi-tenancy — FindBug works out of the box because its tables aren't part of your tenant data model.</p>
|
|
779
|
+
</div>
|
|
780
|
+
|
|
781
|
+
<h3>PostgreSQL + Apartment (schema-per-tenant)</h3>
|
|
782
|
+
<p>FindBug's tables must live in the public schema, and the dashboard path must bypass tenant switching.</p>
|
|
783
|
+
|
|
784
|
+
<h4>1. Exclude FindBug models</h4>
|
|
785
|
+
<pre><code><span class="var">Apartment</span>.configure <span class="kw">do</span> |config|
|
|
786
|
+
config.excluded_models = %w[
|
|
787
|
+
<span class="var">Findbug</span>::<span class="var">ErrorEvent</span>
|
|
788
|
+
<span class="var">Findbug</span>::<span class="var">PerformanceEvent</span>
|
|
789
|
+
<span class="var">Findbug</span>::<span class="var">AlertChannel</span>
|
|
790
|
+
]
|
|
791
|
+
<span class="kw">end</span></code></pre>
|
|
792
|
+
|
|
793
|
+
<h4>2. Exclude the dashboard path from your tenant elevator</h4>
|
|
794
|
+
<pre><code><span class="kw">class</span> <span class="var">SwitchTenantMiddleware</span> < <span class="var">Apartment</span>::<span class="var">Elevators</span>::<span class="var">Generic</span>
|
|
795
|
+
EXCLUDED_PATHS = %w[/findbug].freeze
|
|
796
|
+
|
|
797
|
+
<span class="kw">def</span> parse_tenant_name(request)
|
|
798
|
+
<span class="kw">return</span> nil <span class="kw">if</span> <span class="var">EXCLUDED_PATHS</span>.any? { |p| request.path.start_with?(p) }
|
|
799
|
+
<span class="com"># ... your tenant logic</span>
|
|
800
|
+
<span class="kw">end</span>
|
|
801
|
+
<span class="kw">end</span></code></pre>
|
|
802
|
+
|
|
803
|
+
<h4>3. Run migrations in the public schema</h4>
|
|
804
|
+
<p>Use <code>rails db:migrate</code> as normal — Apartment will skip the excluded models when iterating through tenants, so the FindBug tables are created once in <code>public</code>.</p>
|
|
805
|
+
|
|
806
|
+
<h3>MySQL + Apartment (database-per-tenant)</h3>
|
|
807
|
+
<p>
|
|
808
|
+
MySQL has no schemas in the PostgreSQL sense — <code>SCHEMA</code> is just an alias for
|
|
809
|
+
<code>DATABASE</code>. Apartment isolates tenants by giving each one its own MySQL database
|
|
810
|
+
and switching the connection per request. This works, but FindBug's shared tables need a home.
|
|
811
|
+
</p>
|
|
812
|
+
<p>Two viable approaches:</p>
|
|
813
|
+
<ul>
|
|
814
|
+
<li>
|
|
815
|
+
<strong>Recommended — separate connection</strong>:
|
|
816
|
+
point FindBug's models at a dedicated "operational" database via
|
|
817
|
+
<code>connects_to</code>. Keeps FindBug's data fully isolated from tenant data,
|
|
818
|
+
and avoids the cross-database <code>GRANT</code>/reference gymnastics.
|
|
819
|
+
</li>
|
|
820
|
+
<li>
|
|
821
|
+
<strong>Shared base database</strong>: store FindBug tables in your "main" database
|
|
822
|
+
(the one Apartment connects to before switching). You'll need
|
|
823
|
+
<code>GRANT SELECT, INSERT, UPDATE, DELETE</code> on those tables for the
|
|
824
|
+
per-tenant DB user, and to reference them as
|
|
825
|
+
<code>main_db.findbug_error_events</code> if your tenant connections can't see them by default.
|
|
826
|
+
</li>
|
|
827
|
+
</ul>
|
|
828
|
+
<p>In both cases, you still:</p>
|
|
829
|
+
<ul>
|
|
830
|
+
<li>Add FindBug models to <code>Apartment.excluded_models</code> so it doesn't try to migrate them per tenant.</li>
|
|
831
|
+
<li>Exclude <code>/findbug</code> from your tenant elevator (same code as the PostgreSQL example above).</li>
|
|
832
|
+
</ul>
|
|
833
|
+
|
|
834
|
+
<h3>SQLite multi-tenancy</h3>
|
|
835
|
+
<p>
|
|
836
|
+
SQLite has no schemas and no concept of "current database" — Apartment's only option is
|
|
837
|
+
a separate <code>.sqlite3</code> file per tenant. This is rarely used in real applications
|
|
838
|
+
and pairs poorly with FindBug (no shared place for the FindBug tables to live without an
|
|
839
|
+
<code>ATTACH</code> dance on every connection).
|
|
840
|
+
</p>
|
|
841
|
+
<p>
|
|
842
|
+
If you're on SQLite and need tenant isolation, use <strong>row-level multi-tenancy</strong>
|
|
843
|
+
instead (a <code>tenant_id</code> column on your domain tables, scoped via
|
|
844
|
+
<code>default_scope</code> or a gem like <code>acts_as_tenant</code>).
|
|
845
|
+
FindBug's tables aren't tenant-scoped, so no extra setup is needed.
|
|
846
|
+
</p>
|
|
847
|
+
|
|
848
|
+
<h2 id="activejob-mode">ActiveJob mode</h2>
|
|
849
|
+
<p>By default a built-in background thread handles persistence. To use ActiveJob instead (with Sidekiq, GoodJob, Solid Queue, etc.):</p>
|
|
850
|
+
<pre><code><span class="com"># config/initializers/findbug.rb</span>
|
|
851
|
+
config.auto_persist = <span class="kw">false</span></code></pre>
|
|
852
|
+
<p>Then schedule the jobs with your scheduler of choice:</p>
|
|
853
|
+
<pre><code><span class="com"># Every 30 seconds</span>
|
|
854
|
+
<span class="var">Findbug</span>::<span class="var">PersistJob</span>.perform_later
|
|
855
|
+
|
|
856
|
+
<span class="com"># Daily</span>
|
|
857
|
+
<span class="var">Findbug</span>::<span class="var">CleanupJob</span>.perform_later</code></pre>
|
|
858
|
+
<p>The queue name is taken from <code>config.queue_name</code> (default <code>"findbug"</code>).</p>
|
|
859
|
+
|
|
860
|
+
<!-- ============================================================
|
|
861
|
+
REFERENCE
|
|
862
|
+
============================================================ -->
|
|
863
|
+
|
|
864
|
+
<h2 id="api-reference">API reference</h2>
|
|
865
|
+
|
|
866
|
+
<h3>Module-level</h3>
|
|
867
|
+
<table>
|
|
868
|
+
<thead><tr><th>Method</th><th>Description</th></tr></thead>
|
|
869
|
+
<tbody>
|
|
870
|
+
<tr><td><code>Findbug.config</code></td><td>Access the singleton <code>Configuration</code> object.</td></tr>
|
|
871
|
+
<tr><td><code>Findbug.configure { |c| ... }</code></td><td>Configure FindBug at startup. Validates and returns the config.</td></tr>
|
|
872
|
+
<tr><td><code>Findbug.enabled?</code></td><td><code>true</code> when both <code>config.enabled</code> and <code>config.redis_url</code> are set.</td></tr>
|
|
873
|
+
<tr><td><code>Findbug.capture_exception(exception, context = {})</code></td><td>Capture a rescued exception with optional context.</td></tr>
|
|
874
|
+
<tr><td><code>Findbug.capture_message(message, level = :info, context = {})</code></td><td>Capture a non-exception event.</td></tr>
|
|
875
|
+
<tr><td><code>Findbug.track_performance(name) { ... }</code></td><td>Wrap a block in a custom performance transaction.</td></tr>
|
|
876
|
+
<tr><td><code>Findbug.logger</code></td><td>Internal logger — defaults to <code>Rails.logger</code>.</td></tr>
|
|
877
|
+
<tr><td><code>Findbug.reset!</code></td><td>Reset cached config/logger/pool. Useful in tests.</td></tr>
|
|
878
|
+
</tbody>
|
|
879
|
+
</table>
|
|
880
|
+
|
|
881
|
+
<h3>Controller helpers</h3>
|
|
882
|
+
<table>
|
|
883
|
+
<thead><tr><th>Method</th><th>Description</th></tr></thead>
|
|
884
|
+
<tbody>
|
|
885
|
+
<tr><td><code>findbug_set_user(user)</code></td><td>Attach the current user (id / email / username are auto-extracted).</td></tr>
|
|
886
|
+
<tr><td><code>findbug_set_context(hash)</code></td><td>Deep-merge keys into the current request's context.</td></tr>
|
|
887
|
+
<tr><td><code>findbug_tag(key, value)</code></td><td>Add a single filterable tag.</td></tr>
|
|
888
|
+
<tr><td><code>findbug_breadcrumb(message, category:, data: {})</code></td><td>Record a breadcrumb for the current request.</td></tr>
|
|
889
|
+
<tr><td><code>findbug_capture(exception, extra = {})</code></td><td>Capture an exception with the current request context already merged.</td></tr>
|
|
890
|
+
</tbody>
|
|
891
|
+
</table>
|
|
892
|
+
|
|
893
|
+
<h3>Adapter helper</h3>
|
|
894
|
+
<p>Useful if you're extending FindBug or writing your own migrations against the same multi-DB strategy:</p>
|
|
895
|
+
<table>
|
|
896
|
+
<thead><tr><th>Method</th><th>Returns</th></tr></thead>
|
|
897
|
+
<tbody>
|
|
898
|
+
<tr><td><code>Findbug::AdapterHelper.adapter_name</code></td><td>Lower-cased name, e.g. <code>"postgresql"</code>.</td></tr>
|
|
899
|
+
<tr><td><code>Findbug::AdapterHelper.postgresql? / mysql? / sqlite?</code></td><td>Boolean predicates.</td></tr>
|
|
900
|
+
<tr><td><code>Findbug::AdapterHelper.json_column_type</code></td><td><code>:jsonb</code>, <code>:json</code>, or <code>:text</code>.</td></tr>
|
|
901
|
+
<tr><td><code>Findbug::AdapterHelper.json_default(value)</code></td><td>Adapter-appropriate column default for a JSON field.</td></tr>
|
|
902
|
+
<tr><td><code>Findbug::AdapterHelper.date_trunc_sql(interval, column)</code></td><td>Truncate-by-time SQL for <code>"minute"</code>, <code>"hour"</code>, or <code>"day"</code>.</td></tr>
|
|
903
|
+
</tbody>
|
|
904
|
+
</table>
|
|
905
|
+
|
|
906
|
+
<h2 id="architecture">Architecture</h2>
|
|
907
|
+
<p>FindBug is built around one rule: <strong>never block the user's request</strong>. Here's how a captured event flows through the system:</p>
|
|
908
|
+
<ol>
|
|
909
|
+
<li><strong>Exception occurs</strong> in your app.</li>
|
|
910
|
+
<li><strong>Middleware catches it</strong> (or you call <code>Findbug.capture_exception</code>).</li>
|
|
911
|
+
<li><strong>Scrubber filters</strong> sensitive fields from params, headers, and context.</li>
|
|
912
|
+
<li><strong>Redis buffer write</strong> — a background thread pushes the event to a Redis list. ~1–2 ms, never blocks the request.</li>
|
|
913
|
+
<li><strong>Circuit breaker</strong> — if Redis is unreachable, after 5 failures the breaker opens and capture is silently skipped for 30 seconds.</li>
|
|
914
|
+
<li><strong>Background persister</strong> — every <code>persist_interval</code> seconds (default 30s) pulls batches of events from Redis and bulk-inserts into your DB.</li>
|
|
915
|
+
<li><strong>Aggregation</strong> — errors are grouped by fingerprint (class + normalised backtrace). Repeats increment <code>occurrence_count</code>; perf events are stored individually for percentile analysis.</li>
|
|
916
|
+
<li><strong>Dashboard</strong> queries the DB on demand.</li>
|
|
917
|
+
</ol>
|
|
918
|
+
|
|
919
|
+
<h3>Why a dedicated Redis connection pool?</h3>
|
|
920
|
+
<p>
|
|
921
|
+
FindBug maintains its own connection pool, separate from your app's Redis (or Sidekiq's).
|
|
922
|
+
That way a spike in error capture can't starve your cache or job queue of connections.
|
|
923
|
+
</p>
|
|
924
|
+
|
|
925
|
+
<h3>Why aggregate by fingerprint?</h3>
|
|
926
|
+
<p>
|
|
927
|
+
A single bug can fire thousands of times. Without aggregation, each one would be its own
|
|
928
|
+
row — making the dashboard useless. The fingerprint groups events by exception class and
|
|
929
|
+
the top stack frames, so you see "this bug occurred 1,243 times" rather than 1,243 rows.
|
|
930
|
+
</p>
|
|
931
|
+
|
|
932
|
+
<h2 id="testing">Testing</h2>
|
|
933
|
+
<p>FindBug ships with a comprehensive RSpec suite. Clone the repo and run it locally:</p>
|
|
934
|
+
<pre><code><span class="com">$</span> git clone https://github.com/ITSSOUMIT/findbug.git
|
|
935
|
+
<span class="com">$</span> <span class="kw">cd</span> findbug
|
|
936
|
+
<span class="com">$</span> bundle install
|
|
937
|
+
<span class="com">$</span> bundle exec rspec</code></pre>
|
|
938
|
+
|
|
939
|
+
<p>The suite runs against an in-memory SQLite database to exercise the adapter-agnostic code paths end-to-end, with adapter detection stubbed for PostgreSQL and MySQL-specific behaviour.</p>
|
|
940
|
+
|
|
941
|
+
<h3>Disabling FindBug in tests</h3>
|
|
942
|
+
<p>The default initializer already does this:</p>
|
|
943
|
+
<pre><code>config.enabled = !<span class="var">Rails</span>.env.test?</code></pre>
|
|
944
|
+
|
|
945
|
+
<h2 id="troubleshooting">Troubleshooting</h2>
|
|
946
|
+
|
|
947
|
+
<h3>"Dashboard returns 401 Unauthorized"</h3>
|
|
948
|
+
<p>Make sure both <code>FINDBUG_USERNAME</code> and <code>FINDBUG_PASSWORD</code> are set in the environment. The dashboard is automatically disabled when either is blank — that's a safety feature.</p>
|
|
949
|
+
|
|
950
|
+
<h3>"Errors are captured but never show up in the dashboard"</h3>
|
|
951
|
+
<p>The background persister might not be running. Check:</p>
|
|
952
|
+
<ul>
|
|
953
|
+
<li><code>rails findbug:status</code> — does it show a non-zero <em>Error Queue Length</em>?</li>
|
|
954
|
+
<li>If yes, run <code>rails findbug:flush</code> to drain manually.</li>
|
|
955
|
+
<li>Confirm <code>auto_persist</code> is <code>true</code> (or that you've scheduled <code>Findbug::PersistJob</code> in ActiveJob mode).</li>
|
|
956
|
+
</ul>
|
|
957
|
+
|
|
958
|
+
<h3>"Circuit breaker is open"</h3>
|
|
959
|
+
<p>FindBug stopped trying to write to Redis after 5 consecutive failures. It will retry automatically after 30 seconds. Check that <code>config.redis_url</code> points to a reachable Redis instance.</p>
|
|
960
|
+
|
|
961
|
+
<h3>"Migrations fail on MySQL with 'JSON column can't have a default value'"</h3>
|
|
962
|
+
<p>As of v0.5.0 the migrations skip the default on MySQL automatically (MySQL pre-8.0.13 doesn't support JSON defaults). If you generated the migrations on an older FindBug version, regenerate them with <code>rails generate findbug:install</code> and re-run <code>db:migrate</code>.</p>
|
|
963
|
+
|
|
964
|
+
<h3>"`uninitialized constant Findbug::AdapterHelper`" in a migration</h3>
|
|
965
|
+
<p>This typically happens if the FindBug gem isn't loaded when the migration runs — make sure it's in your <code>Gemfile</code> outside any group restrictions (or include the <code>db</code> group your migrations run under).</p>
|
|
966
|
+
|
|
967
|
+
<div class="docs-footer">
|
|
968
|
+
<div>FindBug v0.5.0 · MIT License · Built by <a href="https://github.com/ITSSOUMIT" target="_blank" style="color: hsl(var(--foreground)); text-decoration: underline;">Soumit Das</a></div>
|
|
969
|
+
<div><a href="https://github.com/ITSSOUMIT/findbug" target="_blank" style="color: hsl(var(--foreground)); text-decoration: underline;">GitHub →</a></div>
|
|
970
|
+
</div>
|
|
971
|
+
|
|
972
|
+
</main>
|
|
973
|
+
</div>
|
|
974
|
+
|
|
975
|
+
</body>
|
|
976
|
+
</html>
|