tackbox 0.1.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.
- package/LICENSE +21 -0
- package/README.md +552 -0
- package/bin/tackbox-eslint.js +147 -0
- package/bin/tackbox-mdlint.js +45 -0
- package/eslint.config.preset.js +21 -0
- package/js/README.md +209 -0
- package/js/eslint-plugin.js +48 -0
- package/js/markdownlint-rules/no-non-ascii.js +132 -0
- package/js/report.js +187 -0
- package/js/rules/_shared.js +809 -0
- package/js/rules/no-broad-notify.js +47 -0
- package/js/rules/no-console-error.js +23 -0
- package/js/rules/no-focused-test.js +22 -0
- package/js/rules/no-parse-fallback.js +150 -0
- package/js/rules/no-skipped-test.js +95 -0
- package/js/rules/no-swallow-allsettled.js +52 -0
- package/js/rules/no-swallow-catch.js +31 -0
- package/js/rules/no-swallow-promise-catch.js +38 -0
- package/js/rules/no-throw-and-report.js +30 -0
- package/js/rules/ts-exit-in-catch.js +26 -0
- package/js/rules/ts-rethrow-without-cause.js +62 -0
- package/js/rules/ts-useless-catch.js +25 -0
- package/js/rules/valid-dedup-key.js +50 -0
- package/js/rules/valid-error-report.js +99 -0
- package/js/tests/eslint-wrapper.test.js +53 -0
- package/js/tests/mdlint-wrapper.test.js +85 -0
- package/js/tests/no-non-ascii.test.js +124 -0
- package/js/tests/report.test.js +182 -0
- package/js/tests/reporters.test.js +223 -0
- package/js/tests/rules.test.js +650 -0
- package/js/tests/svelte.test.js +61 -0
- package/package.json +43 -0
package/js/report.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Browser report helper: empty DSN = log-only no-op.
|
|
2
|
+
// Mirrors the gmux/sts-wand pattern — single funnel for toast,
|
|
3
|
+
// Sentry capture, optional diagnostic stream.
|
|
4
|
+
|
|
5
|
+
const Sentry = require('@sentry/browser')
|
|
6
|
+
|
|
7
|
+
let ready = false
|
|
8
|
+
let rateWindow = 60_000
|
|
9
|
+
let flushTimeout = 2_000
|
|
10
|
+
const lastSent = new Map()
|
|
11
|
+
|
|
12
|
+
function init(opts = {}) {
|
|
13
|
+
const dsn = opts.dsn || ''
|
|
14
|
+
if (!dsn) {
|
|
15
|
+
if (!opts.silentMissing) {
|
|
16
|
+
console.log('[tackbox] WARN report: DSN unset, capture disabled, running log-only')
|
|
17
|
+
}
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
Sentry.init({
|
|
21
|
+
dsn,
|
|
22
|
+
release: opts.release,
|
|
23
|
+
environment: opts.environment,
|
|
24
|
+
debug: !!opts.debug,
|
|
25
|
+
defaultIntegrations: false,
|
|
26
|
+
integrations: opts.integrations || [],
|
|
27
|
+
})
|
|
28
|
+
ready = true
|
|
29
|
+
if (opts.rateWindow > 0) rateWindow = opts.rateWindow
|
|
30
|
+
if (opts.flushTimeout > 0) flushTimeout = opts.flushTimeout
|
|
31
|
+
if (opts.verify) {
|
|
32
|
+
const ok = verify(opts.verifyTimeout || 3_000)
|
|
33
|
+
ok.then(success => {
|
|
34
|
+
if (success) console.log('[tackbox] report: capture verified, DSN=' + maskDSN(dsn))
|
|
35
|
+
else console.warn('[tackbox] report.Init verify: flush timeout, capture endpoint unreachable or rejecting')
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
console.log('[tackbox] report: capture enabled (unverified), DSN=' + maskDSN(dsn))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isReady() { return ready }
|
|
43
|
+
|
|
44
|
+
async function verify(timeout) {
|
|
45
|
+
if (!ready) return false
|
|
46
|
+
Sentry.withScope(scope => {
|
|
47
|
+
scope.setLevel('info')
|
|
48
|
+
scope.setFingerprint(['report.startup'])
|
|
49
|
+
scope.setTag('healthcheck', 'true')
|
|
50
|
+
Sentry.captureMessage('report.Verify')
|
|
51
|
+
})
|
|
52
|
+
return await Sentry.flush(timeout)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function flush(timeout) {
|
|
56
|
+
if (!ready) return
|
|
57
|
+
await Sentry.flush(timeout || flushTimeout)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldDrop(key) {
|
|
61
|
+
if (!key) return false
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
const prev = lastSent.get(key)
|
|
64
|
+
if (prev !== undefined && now - prev < rateWindow) return true
|
|
65
|
+
lastSent.set(key, now)
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// captureEvent is the gated Sentry sink: nothing before init or inside the
|
|
70
|
+
// rate window. Shared by emit (report*) and reportQuiet.
|
|
71
|
+
function captureEvent(level, msg, cause, tags, dedupKey) {
|
|
72
|
+
if (!ready || shouldDrop(dedupKey)) return
|
|
73
|
+
const causeErr = cause instanceof Error ? cause : (cause == null ? null : new Error(String(cause)))
|
|
74
|
+
Sentry.withScope(scope => {
|
|
75
|
+
scope.setLevel(level)
|
|
76
|
+
if (dedupKey) scope.setFingerprint([dedupKey])
|
|
77
|
+
if (tags) for (const k of Object.keys(tags)) scope.setTag(k, String(tags[k]))
|
|
78
|
+
Sentry.captureException(causeErr ? new Error(msg, { cause: causeErr }) : new Error(msg))
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function emit(level, msg, cause, tags, dedupKey) {
|
|
83
|
+
console[level === 'error' ? 'error' : 'warn'](`[${level.toUpperCase()}] ${msg}:`, cause)
|
|
84
|
+
// D005: user lane delivers always, before the init + rate-window gate
|
|
85
|
+
dispatchEventSafely('tackbox:error', { msg, cause, tags, dedupKey, level })
|
|
86
|
+
captureEvent(level, msg, cause, tags, dedupKey)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reportError(msg, cause, tags, dedupKey) {
|
|
90
|
+
emit('error', msg, cause, tags, dedupKey)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function reportWarn(msg, cause, tags, dedupKey) {
|
|
94
|
+
emit('warning', msg, cause, tags, dedupKey)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function reportSynthError(msg, tags, dedupKey) {
|
|
98
|
+
// synth has no caught error; null keeps the capture a plain Error(msg)
|
|
99
|
+
emit('error', msg, null, tags, dedupKey)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// reportQuiet: warning-level capture with no user lane. For background /
|
|
103
|
+
// self-healed / degraded-with-fallback failures.
|
|
104
|
+
function reportQuiet(msg, cause, tags, dedupKey) {
|
|
105
|
+
console.warn(`[QUIET] ${msg}:`, cause)
|
|
106
|
+
captureEvent('warning', msg, cause, tags, dedupKey)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// notify: user lane only, no capture and no rate-window state touched, so a
|
|
110
|
+
// following reportError/reportWarn with the same dedupKey still captures. For
|
|
111
|
+
// an expected environmental fault (the user lost connectivity). cause is the
|
|
112
|
+
// caught error the notice is about.
|
|
113
|
+
function notify(msg, cause, tags, dedupKey) {
|
|
114
|
+
console.warn(`[NOTICE] ${msg}:`, cause)
|
|
115
|
+
dispatchEventSafely('tackbox:error', { msg, cause, tags, dedupKey, level: 'notice' })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reportPanic(name, recovered) {
|
|
119
|
+
const key = 'panic:' + name
|
|
120
|
+
console.error(`[FATAL] panic in ${name}:`, recovered)
|
|
121
|
+
dispatchEventSafely('tackbox:error', { msg: 'panic in ' + name, cause: recovered, tags: { source: name }, dedupKey: key, level: 'fatal' })
|
|
122
|
+
if (!ready || shouldDrop(key)) return
|
|
123
|
+
Sentry.withScope(scope => {
|
|
124
|
+
scope.setLevel('fatal')
|
|
125
|
+
scope.setTag('source', name)
|
|
126
|
+
scope.setFingerprint([key])
|
|
127
|
+
Sentry.captureException(recovered instanceof Error ? recovered : new Error(String(recovered)))
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function setupGlobalHandlers() {
|
|
132
|
+
if (typeof window === 'undefined') return
|
|
133
|
+
window.addEventListener('error', e => {
|
|
134
|
+
reportError('uncaught global error from window', e.error || e.message, { source: 'window.error' }, 'global.uncaught')
|
|
135
|
+
})
|
|
136
|
+
window.addEventListener('unhandledrejection', e => {
|
|
137
|
+
reportError('unhandled promise rejection from window', e.reason, { source: 'window.unhandledrejection' }, 'global.unhandled')
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function maskDSN(dsn) {
|
|
142
|
+
// no-report: malformed user DSN, opaque marker is the recovery
|
|
143
|
+
try {
|
|
144
|
+
const u = new URL(dsn)
|
|
145
|
+
return u.host + u.pathname
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return '<malformed>'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let dispatching = false
|
|
152
|
+
|
|
153
|
+
function dispatchEventSafely(name, detail) {
|
|
154
|
+
if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
|
|
155
|
+
// Re-entrancy guard: a throwing `tackbox:error` listener surfaces via
|
|
156
|
+
// window.onerror (the DOM routes listener failures there, not to dispatchEvent),
|
|
157
|
+
// which setupGlobalHandlers turns back into reportError -> dispatch on the same
|
|
158
|
+
// stack. Skip the nested dispatch so that cannot loop; sequential dispatches
|
|
159
|
+
// are unaffected (D005 deliver-always intact).
|
|
160
|
+
if (dispatching) {
|
|
161
|
+
console.warn('[tackbox] report: nested tackbox:error dispatch skipped (listener-failure re-entry)')
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
dispatching = true
|
|
165
|
+
// no-report: dispatch failure loses only the notice; the verb's local log already ran
|
|
166
|
+
try {
|
|
167
|
+
window.dispatchEvent(new CustomEvent(name, { detail }))
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// dispatch failed
|
|
170
|
+
} finally {
|
|
171
|
+
dispatching = false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
init,
|
|
177
|
+
flush,
|
|
178
|
+
isReady,
|
|
179
|
+
verify,
|
|
180
|
+
reportError,
|
|
181
|
+
reportWarn,
|
|
182
|
+
reportQuiet,
|
|
183
|
+
reportSynthError,
|
|
184
|
+
notify,
|
|
185
|
+
reportPanic,
|
|
186
|
+
setupGlobalHandlers,
|
|
187
|
+
}
|