devbench 0.7.0 → 0.8.1
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/LICENSE +21 -0
- data/README.md +82 -3
- data/lib/devbench/active_job_hooks.rb +121 -0
- data/lib/devbench/config.rb +6 -1
- data/lib/devbench/current.rb +49 -0
- data/lib/devbench/direct.rb +36 -11
- data/lib/devbench/fingerprint.rb +17 -3
- data/lib/devbench/identity.rb +17 -3
- data/lib/devbench/logs.rb +1 -1
- data/lib/devbench/middleware.rb +58 -2
- data/lib/devbench/rails_hooks.rb +1 -1
- data/lib/devbench/railtie.rb +7 -1
- data/lib/devbench/reporter.rb +20 -8
- data/lib/devbench/request_signals.rb +359 -0
- data/lib/devbench/scrub.rb +72 -4
- data/lib/devbench/session.rb +38 -9
- data/lib/devbench/sidecar_transport.rb +1 -1
- data/lib/devbench/sidekiq_hooks.rb +1 -1
- data/lib/devbench/trace.rb +1 -1
- data/lib/devbench/transport.rb +13 -5
- data/lib/devbench/version.rb +18 -6
- data/lib/devbench/view_helper.rb +24 -1
- data/lib/devbench/widget_identity.rb +125 -0
- data/lib/devbench.rb +5 -0
- metadata +7 -3
data/lib/devbench/session.rb
CHANGED
|
@@ -12,9 +12,11 @@ module Devbench
|
|
|
12
12
|
# trace propagation mints one and injects it into HTML responses.
|
|
13
13
|
#
|
|
14
14
|
# This must agree byte for byte with the Go SDK, because a token minted by a
|
|
15
|
-
# Rails app is verified by ingest
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# Rails app is verified by ingest, which uses the Go SDK's VerifySession.
|
|
16
|
+
# Divergence produces a request that succeeds locally and is rejected in
|
|
17
|
+
# production, with nothing to say why. Both SDKs are held to the shared
|
|
18
|
+
# vectors in testdata/session_vectors.json (#181, #563): test/session_test.rb
|
|
19
|
+
# here, sdk/server/go/session_kat_test.go there.
|
|
18
20
|
module Session
|
|
19
21
|
HEADER = 'x-adt-session'
|
|
20
22
|
META_NAME = 'adt-session'
|
|
@@ -25,6 +27,7 @@ module Devbench
|
|
|
25
27
|
# rarely — a short TTL would expire mid-session and force a refresh endpoint,
|
|
26
28
|
# which is more surface in the customer's app for no security gain.
|
|
27
29
|
DEFAULT_TTL = 3600
|
|
30
|
+
# Bytes, not characters, as in the Go SDK.
|
|
28
31
|
MAX_SUBJECT = 128
|
|
29
32
|
|
|
30
33
|
class Expired < StandardError; end
|
|
@@ -38,7 +41,7 @@ module Devbench
|
|
|
38
41
|
raise ArgumentError, 'secret is required' if secret.nil? || secret.strip.empty?
|
|
39
42
|
raise ArgumentError, 'tenant is required' if tenant.nil? || tenant.strip.empty?
|
|
40
43
|
|
|
41
|
-
subject = subject.to_s
|
|
44
|
+
subject = truncate_subject(subject.to_s)
|
|
42
45
|
ttl = DEFAULT_TTL if ttl.nil? || ttl <= 0
|
|
43
46
|
|
|
44
47
|
# The payload is pipe-delimited, so a pipe in a field would let a crafted
|
|
@@ -47,7 +50,9 @@ module Devbench
|
|
|
47
50
|
raise ArgumentError, "tenant and subject must not contain '|'"
|
|
48
51
|
end
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
# Bytes throughout: the signature covers the base64 of these exact bytes,
|
|
54
|
+
# whatever encoding the caller's strings carry.
|
|
55
|
+
payload = [tenant.b, (Time.now.to_i + ttl).to_s, subject.b].join('|')
|
|
51
56
|
encoded = b64(payload)
|
|
52
57
|
|
|
53
58
|
"#{PREFIX}.#{encoded}.#{sign(secret, encoded)}"
|
|
@@ -58,7 +63,9 @@ module Devbench
|
|
|
58
63
|
raise Invalid if secret.nil? || secret.strip.empty?
|
|
59
64
|
raise Invalid unless token.is_a?(String)
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
# -1 keeps trailing empty fields, as Go's strings.Split does: without
|
|
67
|
+
# it "adts1.<payload>.<sig>." is three parts here and four in ingest.
|
|
68
|
+
parts = token.strip.split('.', -1)
|
|
62
69
|
raise Invalid unless parts.length == 3 && parts[0] == PREFIX
|
|
63
70
|
|
|
64
71
|
# Constant time: a signature check that leaks timing can be brute-forced
|
|
@@ -81,6 +88,22 @@ module Devbench
|
|
|
81
88
|
Token.new(fields[0], fields[2], Time.at(expires))
|
|
82
89
|
end
|
|
83
90
|
|
|
91
|
+
# At most MAX_SUBJECT bytes without splitting a UTF-8 character: backs off
|
|
92
|
+
# over at most three continuation bytes, exactly as the Go SDK's
|
|
93
|
+
# truncateSubject does, so a long subject mints the same token in both.
|
|
94
|
+
def self.truncate_subject(subject)
|
|
95
|
+
bytes = subject.b
|
|
96
|
+
return subject if bytes.bytesize <= MAX_SUBJECT
|
|
97
|
+
|
|
98
|
+
cut = MAX_SUBJECT
|
|
99
|
+
3.times do
|
|
100
|
+
break if cut.zero? || (bytes.getbyte(cut) & 0xC0) != 0x80
|
|
101
|
+
|
|
102
|
+
cut -= 1
|
|
103
|
+
end
|
|
104
|
+
bytes.byteslice(0, cut).force_encoding(subject.encoding)
|
|
105
|
+
end
|
|
106
|
+
|
|
84
107
|
def self.sign(secret, encoded_payload)
|
|
85
108
|
b64(OpenSSL::HMAC.digest('SHA256', secret, encoded_payload))
|
|
86
109
|
end
|
|
@@ -90,10 +113,16 @@ module Devbench
|
|
|
90
113
|
Base64.urlsafe_encode64(bytes, padding: false)
|
|
91
114
|
end
|
|
92
115
|
|
|
116
|
+
# Decodes as Go's base64.RawURLEncoding does, and refuses what it refuses:
|
|
117
|
+
# padding (=), the standard alphabet's + and /, whitespace, and a length
|
|
118
|
+
# that leaves one character over. (Base64.urlsafe_decode64 accepts the
|
|
119
|
+
# first two, so a padded payload verified here but not in ingest, #563.)
|
|
120
|
+
# Like Go, non-zero bits left over in the last character are ignored.
|
|
93
121
|
def self.unb64(text)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
return nil unless text.match?(/\A[A-Za-z0-9_-]*\z/) && text.length % 4 != 1
|
|
123
|
+
|
|
124
|
+
std = text.tr('-_', '+/')
|
|
125
|
+
(std + ('=' * ((4 - (std.length % 4)) % 4))).unpack1('m')
|
|
97
126
|
end
|
|
98
127
|
end
|
|
99
128
|
end
|
|
@@ -5,7 +5,7 @@ require 'socket'
|
|
|
5
5
|
|
|
6
6
|
module Devbench
|
|
7
7
|
# Sidecar mode: each report is one line of JSON on the local sidecar's
|
|
8
|
-
# unix socket (docs/
|
|
8
|
+
# unix socket (docs/server-sdk/control-socket.md, "Control socket — v1"). Used when no
|
|
9
9
|
# DSN is set; unchanged from 0.4.
|
|
10
10
|
#
|
|
11
11
|
# Never blocks for long: this runs inside rescue blocks and exception
|
|
@@ -5,7 +5,7 @@ require_relative 'current'
|
|
|
5
5
|
require_relative 'reporter'
|
|
6
6
|
|
|
7
7
|
module Devbench
|
|
8
|
-
# Native Sidekiq jobs (docs/
|
|
8
|
+
# Native Sidekiq jobs (docs/server-sdk/unhandled-failures.md, Capability 2, `job`).
|
|
9
9
|
#
|
|
10
10
|
# A class that includes Sidekiq::Job never touches ActiveJob, so the
|
|
11
11
|
# perform.active_job hook sees none of its failures. This is the
|
data/lib/devbench/trace.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Devbench
|
|
4
4
|
# The correlation id that ties a user action together across every service.
|
|
5
5
|
#
|
|
6
|
-
# Wire form (docs/
|
|
6
|
+
# Wire form (docs/server-sdk/trace-propagation.md): v1/<session>/<intent>/<hop>
|
|
7
7
|
#
|
|
8
8
|
# This must agree exactly with the Go SDK and the sidecar's extraction regex.
|
|
9
9
|
# A trace the browser writes and Rails mangles is worse than no trace: the
|
data/lib/devbench/transport.rb
CHANGED
|
@@ -4,8 +4,9 @@ require_relative 'config'
|
|
|
4
4
|
require_relative 'sidecar_transport'
|
|
5
5
|
require_relative 'direct'
|
|
6
6
|
require_relative 'logs'
|
|
7
|
+
require_relative 'request_signals'
|
|
7
8
|
|
|
8
|
-
# Configuration and transport selection (docs/
|
|
9
|
+
# Configuration and transport selection (docs/server-sdk/transports.md,
|
|
9
10
|
# "Transports: direct (default) or sidecar (optional)").
|
|
10
11
|
#
|
|
11
12
|
# Exactly one transport per process, decided at the first report:
|
|
@@ -124,15 +125,18 @@ module Devbench
|
|
|
124
125
|
# seconds. A no-op in sidecar mode. Returns true when nothing is left
|
|
125
126
|
# unsent. Never raises.
|
|
126
127
|
def flush!(timeout: 5)
|
|
128
|
+
# Closes a 429 spike window the clock has left, in either transport
|
|
129
|
+
# (sidecar mode has no flush thread of its own to do it).
|
|
130
|
+
RequestSignals.tick!
|
|
127
131
|
transport.flush!(timeout: timeout) ? true : false
|
|
128
132
|
rescue StandardError, SystemStackError
|
|
129
133
|
false
|
|
130
134
|
end
|
|
131
135
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
# (`rake devbench:test` under Rails).
|
|
135
|
-
#
|
|
136
|
+
# Asks ingest whether the DSN's key is valid (`POST /v1/check`) and prints
|
|
137
|
+
# the answer, so an operator can check the DSN without waiting for a
|
|
138
|
+
# flush (`rake devbench:test` under Rails). Creates nothing: no event,
|
|
139
|
+
# no issue. Returns true when ingest accepted the key. Never raises.
|
|
136
140
|
def test!(io: $stdout)
|
|
137
141
|
cfg = config
|
|
138
142
|
unless cfg.enabled?
|
|
@@ -190,6 +194,10 @@ module Devbench
|
|
|
190
194
|
private
|
|
191
195
|
|
|
192
196
|
def reset_transport!
|
|
197
|
+
# A replaced configuration ends the 429 spike window, as process exit
|
|
198
|
+
# does: evaluated as it stands and reported under the transport that
|
|
199
|
+
# saw its requests, before that transport's last flush.
|
|
200
|
+
RequestSignals.finish!(@transport)
|
|
193
201
|
old = TRANSPORT_LOCK.synchronize do
|
|
194
202
|
previous = @transport
|
|
195
203
|
@transport = nil
|
data/lib/devbench/version.rb
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Devbench
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# The wire format
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
# The gem's release version. Each SDK has its own version and release tag;
|
|
5
|
+
# the Ruby release (tag ruby-vX.Y.Z in the public repo) checks this equals
|
|
6
|
+
# the tag. The wire format is PROTOCOL_VERSION below.
|
|
7
|
+
VERSION = '0.8.1'
|
|
8
|
+
|
|
9
|
+
# This SDK's name in every flush's `sdk` field: the key
|
|
10
|
+
# testdata/protocol_vectors.json `sdk.names` gives sdk/server/ruby
|
|
11
|
+
# (docs/server-sdk/wire-protocol.md, "the SDK's name and version"). The
|
|
12
|
+
# field's version is VERSION, never a second literal.
|
|
13
|
+
SDK_NAME = 'ruby'
|
|
14
|
+
|
|
15
|
+
# The wire format this speaks: the `v` every payload carries (the flush
|
|
16
|
+
# body, the evidence bundle, every control-socket message). Not the gem's
|
|
17
|
+
# release VERSION above: it changes only for a breaking wire change, and
|
|
18
|
+
# ingest tells formats apart by it. It must equal `current` in
|
|
19
|
+
# testdata/protocol_vectors.json (docs/server-sdk/protocol-version.md, "Protocol
|
|
20
|
+
# version"); send it, never a literal.
|
|
21
|
+
PROTOCOL_VERSION = 1
|
|
10
22
|
end
|
data/lib/devbench/view_helper.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'erb'
|
|
4
4
|
require_relative 'version'
|
|
5
|
+
require_relative 'current'
|
|
6
|
+
require_relative 'widget_identity'
|
|
5
7
|
|
|
6
8
|
module Devbench
|
|
7
9
|
# The browser sensor's script tag, for the layout's <head>:
|
|
@@ -10,7 +12,7 @@ module Devbench
|
|
|
10
12
|
#
|
|
11
13
|
# renders
|
|
12
14
|
#
|
|
13
|
-
# <script src="https://unpkg.com/devbench@0.
|
|
15
|
+
# <script src="https://unpkg.com/devbench@0.8.1/dist/devbench.min.js"
|
|
14
16
|
# data-dsn="https://<public>@<host>" data-release="<release>" defer></script>
|
|
15
17
|
#
|
|
16
18
|
# The DSN is the public part of DEVBENCH_DSN only (Devbench.browser_dsn),
|
|
@@ -21,6 +23,14 @@ module Devbench
|
|
|
21
23
|
# CSP nonce (content_security_policy_nonce_generator), the tag carries it,
|
|
22
24
|
# as Rails' own `javascript_include_tag nonce: true` does.
|
|
23
25
|
#
|
|
26
|
+
# Chat widget (docs/server-sdk/chat-widget.md, Capability 6): when
|
|
27
|
+
# DEVBENCH_WIDGET_SECRET is set and Devbench.set_user was called for this
|
|
28
|
+
# request, the tag also carries data-widget-identity="<token>", the user
|
|
29
|
+
# (and their role and chat permission) signed at render time. No secret, no
|
|
30
|
+
# user (a signed-out page) or an identity the signer refuses: no
|
|
31
|
+
# attribute, so no widget. A page carrying one is per-user; don't serve it
|
|
32
|
+
# from a shared cache.
|
|
33
|
+
#
|
|
24
34
|
# Included into ActionView by the Railtie. Never raises.
|
|
25
35
|
module ViewHelper
|
|
26
36
|
CDN = 'https://unpkg.com'
|
|
@@ -34,6 +44,8 @@ module Devbench
|
|
|
34
44
|
return devbench_safe('') if dsn.nil?
|
|
35
45
|
|
|
36
46
|
attrs = [['src', ViewHelper.src], ['data-dsn', dsn], ['data-release', Devbench.config.resolved_release]]
|
|
47
|
+
identity = devbench_widget_identity
|
|
48
|
+
attrs << ['data-widget-identity', identity] if identity
|
|
37
49
|
nonce = devbench_nonce
|
|
38
50
|
attrs << ['nonce', nonce] if nonce
|
|
39
51
|
html = attrs.map { |name, value| %(#{name}="#{ERB::Util.html_escape(value)}") }.join(' ')
|
|
@@ -44,6 +56,17 @@ module Devbench
|
|
|
44
56
|
|
|
45
57
|
private
|
|
46
58
|
|
|
59
|
+
# The signed widget identity for this request's user, or nil.
|
|
60
|
+
def devbench_widget_identity
|
|
61
|
+
secret = Devbench.config.widget_secret
|
|
62
|
+
user = Current.widget_user
|
|
63
|
+
return nil if secret.nil? || secret.to_s.empty? || user.nil?
|
|
64
|
+
|
|
65
|
+
WidgetIdentity.sign(secret, email: user[:email], account: user[:account], role: user[:role], chat: user[:chat])
|
|
66
|
+
rescue StandardError, SystemStackError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
47
70
|
# Rails' content_security_policy_nonce: nil unless the app configured a
|
|
48
71
|
# nonce generator.
|
|
49
72
|
def devbench_nonce
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
module Devbench
|
|
7
|
+
# The chat widget's signed identity (docs/server-sdk/chat-widget.md, "Capability 6
|
|
8
|
+
# — Chat widget").
|
|
9
|
+
#
|
|
10
|
+
# dbw1.<base64url(issued_at \n email \n account \n role \n chat)>.<base64url(HMAC-SHA256)>
|
|
11
|
+
#
|
|
12
|
+
# The app's server signs the signed-in user with the tenant's widget secret
|
|
13
|
+
# (DEVBENCH_WIDGET_SECRET); the browser holds only the token, and the chat
|
|
14
|
+
# service shows the widget only to a user whose token verifies. Every signer
|
|
15
|
+
# must reproduce testdata/widget_identity_vectors.json byte for byte, or the
|
|
16
|
+
# widget works in a unit test and never renders in production.
|
|
17
|
+
#
|
|
18
|
+
# Rails apps never call this: devbench_script_tag signs whatever
|
|
19
|
+
# Devbench.set_user recorded for the request.
|
|
20
|
+
module WidgetIdentity
|
|
21
|
+
PREFIX = 'dbw1'
|
|
22
|
+
# The closed list, in the token's canonical (byte) order. A new one is a
|
|
23
|
+
# contract change, made in lockstep with the vectors.
|
|
24
|
+
CAPABILITIES = %w[feature_requests handoff howto report status].freeze
|
|
25
|
+
MAX = 255
|
|
26
|
+
# ASCII whitespace only, as the contract names it. String#strip would
|
|
27
|
+
# also remove a trailing NUL, which must survive the trim to be refused
|
|
28
|
+
# (Go's signer refuses it): the two would disagree.
|
|
29
|
+
TRIM = /\A[ \t\n\r\f\v]+|[ \t\n\r\f\v]+\z/
|
|
30
|
+
# C0 controls and DEL, refused when left in a field after trimming (the
|
|
31
|
+
# trimmed whitespace is C0 too, and fine at the ends). Identities are
|
|
32
|
+
# compared and displayed; invisible bytes could make two look alike.
|
|
33
|
+
CONTROL = /[\x00-\x1f\x7f]/
|
|
34
|
+
NO_CHAT = 'none'
|
|
35
|
+
|
|
36
|
+
# Raised internally for an identity that cannot be signed; never escapes.
|
|
37
|
+
class Refused < StandardError; end
|
|
38
|
+
|
|
39
|
+
# Returns the token, or nil when this identity must not be signed: no
|
|
40
|
+
# secret, neither email nor account, a control character (U+0000-U+001F,
|
|
41
|
+
# U+007F) left in a field after trimming, a field over 255 characters,
|
|
42
|
+
# invalid text, or a chat capability not in CAPABILITIES (exact,
|
|
43
|
+
# lowercase). Refuses rather than repairs: truncating would sign a
|
|
44
|
+
# different person. The first refusal in the process is logged, once.
|
|
45
|
+
#
|
|
46
|
+
# chat: nil states nothing (the tenant's settings decide); [] means the
|
|
47
|
+
# user may not chat; otherwise their capabilities, symbols or strings,
|
|
48
|
+
# in any order, duplicates allowed. Never raises.
|
|
49
|
+
def self.sign(secret, email: nil, account: nil, role: nil, chat: nil, issued_at: Time.now.to_i)
|
|
50
|
+
token(secret.to_s, email, account, role, chat, issued_at)
|
|
51
|
+
rescue Refused => e
|
|
52
|
+
Devbench.warn_once(:widget_identity, "chat widget identity not signed: #{e.message}; " \
|
|
53
|
+
'no chat widget for that user (further refusals are not logged)')
|
|
54
|
+
nil
|
|
55
|
+
rescue StandardError, SystemStackError
|
|
56
|
+
Devbench.warn_once(:widget_identity, 'chat widget identity not signed: unexpected error; ' \
|
|
57
|
+
'no chat widget for that user (further refusals are not logged)')
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.token(secret, email, account, role, chat, issued_at)
|
|
62
|
+
raise Refused, 'no widget secret (DEVBENCH_WIDGET_SECRET)' if secret.empty?
|
|
63
|
+
|
|
64
|
+
email = field(email, 'email')
|
|
65
|
+
account = field(account, 'account')
|
|
66
|
+
role = field(role, 'role')
|
|
67
|
+
# Simple lowercase mapping, as Go's strings.ToLower: Ruby's full
|
|
68
|
+
# mapping turns U+0130 into two characters where Go makes one. It is
|
|
69
|
+
# the only code point the two disagree on.
|
|
70
|
+
email = email.tr("\u0130", 'i').downcase
|
|
71
|
+
raise Refused, 'neither email nor account' if email.empty? && account.empty?
|
|
72
|
+
|
|
73
|
+
ts = Integer(issued_at)
|
|
74
|
+
raise Refused, 'issued before 1970' if ts.negative?
|
|
75
|
+
|
|
76
|
+
raw = [ts.to_s, email, account, role, chat_field(chat)].join("\n")
|
|
77
|
+
payload = b64(raw.b)
|
|
78
|
+
"#{PREFIX}.#{payload}.#{b64(OpenSSL::HMAC.digest('SHA256', secret.b, "#{PREFIX}.#{payload}"))}"
|
|
79
|
+
end
|
|
80
|
+
private_class_method :token
|
|
81
|
+
|
|
82
|
+
# Trimmed, valid UTF-8, no control character, at most MAX characters; else
|
|
83
|
+
# Refused. nil is "".
|
|
84
|
+
def self.field(value, name)
|
|
85
|
+
text = utf8(value, name).gsub(TRIM, '')
|
|
86
|
+
raise Refused, "control character in #{name}" if text.match?(CONTROL)
|
|
87
|
+
raise Refused, "#{name} over #{MAX} characters" if text.length > MAX
|
|
88
|
+
|
|
89
|
+
text
|
|
90
|
+
end
|
|
91
|
+
private_class_method :field
|
|
92
|
+
|
|
93
|
+
def self.utf8(value, name)
|
|
94
|
+
text = value.nil? ? +'' : value.to_s.dup
|
|
95
|
+
text = text.encoding == Encoding::BINARY ? text.force_encoding(Encoding::UTF_8) : text.encode(Encoding::UTF_8)
|
|
96
|
+
raise Refused, "#{name} is not valid UTF-8" unless text.valid_encoding?
|
|
97
|
+
|
|
98
|
+
text
|
|
99
|
+
rescue EncodingError
|
|
100
|
+
raise Refused, "#{name} is not valid UTF-8"
|
|
101
|
+
end
|
|
102
|
+
private_class_method :utf8
|
|
103
|
+
|
|
104
|
+
# "" not stated, "none" may not chat, else known names de-duplicated,
|
|
105
|
+
# sorted by bytes, joined by ",".
|
|
106
|
+
def self.chat_field(chat)
|
|
107
|
+
return '' if chat.nil?
|
|
108
|
+
raise Refused, 'chat must be a list of capabilities' unless chat.is_a?(Array)
|
|
109
|
+
|
|
110
|
+
names = chat.map do |c|
|
|
111
|
+
name = c.is_a?(Symbol) || c.is_a?(String) ? c.to_s : nil
|
|
112
|
+
raise Refused, "unknown chat capability #{c.inspect}" unless CAPABILITIES.include?(name)
|
|
113
|
+
|
|
114
|
+
name
|
|
115
|
+
end
|
|
116
|
+
names.empty? ? NO_CHAT : names.uniq.sort.join(',')
|
|
117
|
+
end
|
|
118
|
+
private_class_method :chat_field
|
|
119
|
+
|
|
120
|
+
def self.b64(bytes)
|
|
121
|
+
Base64.urlsafe_encode64(bytes, padding: false)
|
|
122
|
+
end
|
|
123
|
+
private_class_method :b64
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/devbench.rb
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# * reports unhandled exceptions (Devbench::Middleware, Devbench::Railtie,
|
|
14
14
|
# Devbench::SidekiqHooks,
|
|
15
15
|
# Devbench.capture_exception)
|
|
16
|
+
# * reports failed (5xx) and slow requests (Devbench::Middleware,
|
|
17
|
+
# and 429 spikes Devbench::RequestSignals)
|
|
16
18
|
# * keeps recent log lines per trace (Devbench::Logs; direct mode)
|
|
17
19
|
# * renders the browser sensor's tag (devbench_script_tag)
|
|
18
20
|
#
|
|
@@ -23,6 +25,7 @@ require_relative 'devbench/trace'
|
|
|
23
25
|
require_relative 'devbench/current'
|
|
24
26
|
require_relative 'devbench/backtrace'
|
|
25
27
|
require_relative 'devbench/identity'
|
|
28
|
+
require_relative 'devbench/request_signals'
|
|
26
29
|
require_relative 'devbench/middleware'
|
|
27
30
|
require_relative 'devbench/http'
|
|
28
31
|
require_relative 'devbench/reporter'
|
|
@@ -30,7 +33,9 @@ require_relative 'devbench/session'
|
|
|
30
33
|
require_relative 'devbench/session_middleware'
|
|
31
34
|
require_relative 'devbench/rails_hooks'
|
|
32
35
|
require_relative 'devbench/sidekiq_hooks'
|
|
36
|
+
require_relative 'devbench/active_job_hooks'
|
|
33
37
|
require_relative 'devbench/logs'
|
|
38
|
+
require_relative 'devbench/widget_identity'
|
|
34
39
|
require_relative 'devbench/view_helper'
|
|
35
40
|
|
|
36
41
|
# The pre-0.5 name. The same module object, not a copy: ADT::Middleware *is*
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devbench
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dev Bench
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Reports unhandled exceptions, Rails.error reports, failed ActiveJob and
|
|
@@ -27,9 +27,11 @@ executables: []
|
|
|
27
27
|
extensions: []
|
|
28
28
|
extra_rdoc_files: []
|
|
29
29
|
files:
|
|
30
|
+
- LICENSE
|
|
30
31
|
- README.md
|
|
31
32
|
- lib/adt.rb
|
|
32
33
|
- lib/devbench.rb
|
|
34
|
+
- lib/devbench/active_job_hooks.rb
|
|
33
35
|
- lib/devbench/backtrace.rb
|
|
34
36
|
- lib/devbench/config.rb
|
|
35
37
|
- lib/devbench/current.rb
|
|
@@ -43,6 +45,7 @@ files:
|
|
|
43
45
|
- lib/devbench/rails_hooks.rb
|
|
44
46
|
- lib/devbench/railtie.rb
|
|
45
47
|
- lib/devbench/reporter.rb
|
|
48
|
+
- lib/devbench/request_signals.rb
|
|
46
49
|
- lib/devbench/scrub.rb
|
|
47
50
|
- lib/devbench/session.rb
|
|
48
51
|
- lib/devbench/session_middleware.rb
|
|
@@ -52,10 +55,11 @@ files:
|
|
|
52
55
|
- lib/devbench/transport.rb
|
|
53
56
|
- lib/devbench/version.rb
|
|
54
57
|
- lib/devbench/view_helper.rb
|
|
58
|
+
- lib/devbench/widget_identity.rb
|
|
55
59
|
- lib/generators/devbench/devbench_generator.rb
|
|
56
60
|
homepage: https://github.com/pasperry/devbench-sdk
|
|
57
61
|
licenses:
|
|
58
|
-
-
|
|
62
|
+
- MIT
|
|
59
63
|
metadata:
|
|
60
64
|
rubygems_mfa_required: 'true'
|
|
61
65
|
source_code_uri: https://github.com/pasperry/devbench-sdk
|