devbench 0.5.0 → 0.6.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 +106 -19
- data/lib/devbench/config.rb +66 -21
- data/lib/devbench/direct.rb +172 -23
- data/lib/devbench/egress_policy.rb +101 -0
- data/lib/devbench/logs.rb +342 -0
- data/lib/devbench/railtie.rb +21 -0
- data/lib/devbench/scrub.rb +46 -8
- data/lib/devbench/sidekiq_hooks.rb +4 -0
- data/lib/devbench/transport.rb +65 -10
- data/lib/devbench/version.rb +1 -1
- data/lib/devbench/view_helper.rb +62 -0
- data/lib/devbench.rb +7 -2
- data/lib/generators/devbench/devbench_generator.rb +210 -0
- metadata +8 -2
data/lib/devbench/transport.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'config'
|
|
4
4
|
require_relative 'sidecar_transport'
|
|
5
5
|
require_relative 'direct'
|
|
6
|
+
require_relative 'logs'
|
|
6
7
|
|
|
7
8
|
# Configuration and transport selection (docs/SERVER_SDK_SPEC.md,
|
|
8
9
|
# "Transports: direct (default) or sidecar (optional)").
|
|
@@ -17,8 +18,6 @@ require_relative 'direct'
|
|
|
17
18
|
# Never both: a sidecar on the same host still reads logs, but writing to
|
|
18
19
|
# its socket as well would count every report twice.
|
|
19
20
|
module Devbench
|
|
20
|
-
# Raised (and rescued) by Devbench.test! to make its synthetic report.
|
|
21
|
-
class TestException < StandardError; end
|
|
22
21
|
|
|
23
22
|
TRANSPORT_LOCK = Mutex.new
|
|
24
23
|
CONFIG_LOCK = Mutex.new
|
|
@@ -42,6 +41,9 @@ module Devbench
|
|
|
42
41
|
def configure
|
|
43
42
|
yield config if block_given?
|
|
44
43
|
reset_transport!
|
|
44
|
+
# Hooks already in place follow the new configuration: on in direct
|
|
45
|
+
# mode, inert otherwise.
|
|
46
|
+
Logs.hooked? && direct? ? Logs.activate! : Logs.deactivate!
|
|
45
47
|
nil
|
|
46
48
|
rescue StandardError, SystemStackError => e
|
|
47
49
|
warn_once(:configure, "Devbench.configure raised #{e.class}; reporting is unchanged")
|
|
@@ -60,6 +62,64 @@ module Devbench
|
|
|
60
62
|
@transport || TRANSPORT_LOCK.synchronize { @transport ||= build_transport }
|
|
61
63
|
end
|
|
62
64
|
|
|
65
|
+
# The DSN the browser sensor may hold — https://<public>@host — derived
|
|
66
|
+
# from DEVBENCH_DSN, so a page needs no setting of its own. Nil when the
|
|
67
|
+
# DSN has no public part (a 0.5 single-key DSN: that key is a server
|
|
68
|
+
# secret), does not parse, or reporting is disabled. Never raises, and
|
|
69
|
+
# never returns anything carrying the secret.
|
|
70
|
+
def browser_dsn
|
|
71
|
+
cfg = config
|
|
72
|
+
return nil unless cfg.enabled?
|
|
73
|
+
|
|
74
|
+
raw = cfg.dsn.to_s
|
|
75
|
+
cached = @browser_dsn
|
|
76
|
+
return cached[1] if cached && cached[0] == raw
|
|
77
|
+
|
|
78
|
+
value = raw.strip.empty? ? nil : DSN.parse(raw).browser_dsn
|
|
79
|
+
@browser_dsn = [raw, value]
|
|
80
|
+
value
|
|
81
|
+
rescue StandardError, SystemStackError
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Where the browser sensor connects, for the app's CSP connect_src:
|
|
86
|
+
# the ingest host from DEVBENCH_DSN and evidence storage. Read when the
|
|
87
|
+
# CSP initializer runs, so the policy follows the DSN instead of a host
|
|
88
|
+
# frozen at generate time; empty when there is no usable DSN, so an app
|
|
89
|
+
# without Dev Bench configured (e.g. production) keeps its policy as is.
|
|
90
|
+
# `bin/rails generate devbench` adds `*Devbench.csp_connect_sources`.
|
|
91
|
+
STORAGE_SOURCE = 'https://*.storage.supabase.co'
|
|
92
|
+
|
|
93
|
+
def csp_connect_sources
|
|
94
|
+
return [] if browser_dsn.nil?
|
|
95
|
+
|
|
96
|
+
[DSN.parse(config.dsn.to_s).base, STORAGE_SOURCE]
|
|
97
|
+
rescue StandardError, SystemStackError
|
|
98
|
+
[]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# True when this process reports straight to Dev Bench (a DSN is set
|
|
102
|
+
# and parses). Builds the transport if it was not built yet.
|
|
103
|
+
def direct?
|
|
104
|
+
transport.is_a?(DirectTransport)
|
|
105
|
+
rescue StandardError, SystemStackError
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Keeps recent lines from these loggers, per trace, for triage to ask
|
|
110
|
+
# for (direct mode only; elsewhere a no-op). The Railtie does this for
|
|
111
|
+
# Rails.logger and Sidekiq.logger; a Rack app without Rails calls it
|
|
112
|
+
# itself. Returns true when capture is on. Never raises.
|
|
113
|
+
def capture_logs(*loggers)
|
|
114
|
+
return false unless enabled? && direct?
|
|
115
|
+
|
|
116
|
+
hooked = loggers.flatten.map { |l| Logs.install(l) }.compact
|
|
117
|
+
Logs.activate! unless hooked.empty?
|
|
118
|
+
Logs.active?
|
|
119
|
+
rescue StandardError, SystemStackError
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
|
|
63
123
|
# Sends whatever direct mode has counted, now, waiting at most `timeout`
|
|
64
124
|
# seconds. A no-op in sidecar mode. Returns true when nothing is left
|
|
65
125
|
# unsent. Never raises.
|
|
@@ -81,7 +141,7 @@ module Devbench
|
|
|
81
141
|
end
|
|
82
142
|
if cfg.dsn.nil? || cfg.dsn.strip.empty?
|
|
83
143
|
io.puts 'DEVBENCH_DSN is not set. Set it to the DSN Dev Bench gave you for this ' \
|
|
84
|
-
'environment (https://<
|
|
144
|
+
'environment (https://<public>:<secret>@<host>, from `adt dsn create`), or call Devbench.configure { |c| c.dsn = ... }.'
|
|
85
145
|
return false
|
|
86
146
|
end
|
|
87
147
|
|
|
@@ -92,14 +152,8 @@ module Devbench
|
|
|
92
152
|
return false
|
|
93
153
|
end
|
|
94
154
|
|
|
95
|
-
error = begin
|
|
96
|
-
raise TestException, 'Dev Bench test exception: if you can read this, the DSN works'
|
|
97
|
-
rescue TestException => e
|
|
98
|
-
e
|
|
99
|
-
end
|
|
100
|
-
payload = Reporter.report_for(error, context: 'explicit', handled: true, symbol: 'devbench:test')
|
|
101
155
|
client = DirectTransport.new(dsn: dsn, service: cfg.resolved_service, release: cfg.resolved_release)
|
|
102
|
-
client.self_test(
|
|
156
|
+
client.self_test(io)
|
|
103
157
|
rescue StandardError, SystemStackError => e
|
|
104
158
|
io.puts "Dev Bench test failed: #{e.class}: #{e.message}"
|
|
105
159
|
false
|
|
@@ -110,6 +164,7 @@ module Devbench
|
|
|
110
164
|
# Devbench.configure.
|
|
111
165
|
def reset!
|
|
112
166
|
reset_transport!
|
|
167
|
+
Logs.deactivate!
|
|
113
168
|
CONFIG_LOCK.synchronize { @config = nil }
|
|
114
169
|
nil
|
|
115
170
|
end
|
data/lib/devbench/version.rb
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require_relative 'version'
|
|
5
|
+
|
|
6
|
+
module Devbench
|
|
7
|
+
# The browser sensor's script tag, for the layout's <head>:
|
|
8
|
+
#
|
|
9
|
+
# <%= devbench_script_tag %>
|
|
10
|
+
#
|
|
11
|
+
# renders
|
|
12
|
+
#
|
|
13
|
+
# <script src="https://unpkg.com/devbench@0.6.0/dist/devbench.min.js"
|
|
14
|
+
# data-dsn="https://<public>@<host>" data-release="<release>" defer></script>
|
|
15
|
+
#
|
|
16
|
+
# The DSN is the public part of DEVBENCH_DSN only (Devbench.browser_dsn),
|
|
17
|
+
# so the page needs no setting of its own and the secret never reaches
|
|
18
|
+
# HTML. No public part (no DSN, a 0.5 single-key DSN, reporting disabled)
|
|
19
|
+
# renders nothing at all. The src is pinned to this gem's version, so a
|
|
20
|
+
# gem upgrade upgrades the browser sensor with it. Where the app uses a
|
|
21
|
+
# CSP nonce (content_security_policy_nonce_generator), the tag carries it,
|
|
22
|
+
# as Rails' own `javascript_include_tag nonce: true` does.
|
|
23
|
+
#
|
|
24
|
+
# Included into ActionView by the Railtie. Never raises.
|
|
25
|
+
module ViewHelper
|
|
26
|
+
CDN = 'https://unpkg.com'
|
|
27
|
+
|
|
28
|
+
def self.src
|
|
29
|
+
"#{CDN}/devbench@#{Devbench::VERSION}/dist/devbench.min.js"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def devbench_script_tag
|
|
33
|
+
dsn = Devbench.browser_dsn
|
|
34
|
+
return devbench_safe('') if dsn.nil?
|
|
35
|
+
|
|
36
|
+
attrs = [['src', ViewHelper.src], ['data-dsn', dsn], ['data-release', Devbench.config.resolved_release]]
|
|
37
|
+
nonce = devbench_nonce
|
|
38
|
+
attrs << ['nonce', nonce] if nonce
|
|
39
|
+
html = attrs.map { |name, value| %(#{name}="#{ERB::Util.html_escape(value)}") }.join(' ')
|
|
40
|
+
devbench_safe("<script #{html} defer></script>")
|
|
41
|
+
rescue StandardError, SystemStackError
|
|
42
|
+
devbench_safe('')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Rails' content_security_policy_nonce: nil unless the app configured a
|
|
48
|
+
# nonce generator.
|
|
49
|
+
def devbench_nonce
|
|
50
|
+
return nil unless respond_to?(:content_security_policy_nonce, true)
|
|
51
|
+
|
|
52
|
+
nonce = content_security_policy_nonce
|
|
53
|
+
nonce.nil? || nonce.to_s.empty? ? nil : nonce.to_s
|
|
54
|
+
rescue StandardError, SystemStackError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def devbench_safe(html)
|
|
59
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/devbench.rb
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# Dev Bench server SDK for Ruby/Rails (formerly "ADT"; DECISIONS #160).
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# bundle add devbench
|
|
6
|
+
# bin/rails generate devbench
|
|
7
|
+
# DEVBENCH_DSN=https://<public>:<secret>@<ingest host> (from `adt dsn create`)
|
|
7
8
|
#
|
|
8
9
|
# What it does:
|
|
9
10
|
# * accepts and forwards the correlation id (Devbench::Middleware, Devbench::HTTP)
|
|
@@ -12,6 +13,8 @@
|
|
|
12
13
|
# * reports unhandled exceptions (Devbench::Middleware, Devbench::Railtie,
|
|
13
14
|
# Devbench::SidekiqHooks,
|
|
14
15
|
# Devbench.capture_exception)
|
|
16
|
+
# * keeps recent log lines per trace (Devbench::Logs; direct mode)
|
|
17
|
+
# * renders the browser sensor's tag (devbench_script_tag)
|
|
15
18
|
#
|
|
16
19
|
# The old name is kept: `require 'adt'` and every `ADT.` / `ADT::` call in an
|
|
17
20
|
# existing install resolve to this module.
|
|
@@ -27,6 +30,8 @@ require_relative 'devbench/session'
|
|
|
27
30
|
require_relative 'devbench/session_middleware'
|
|
28
31
|
require_relative 'devbench/rails_hooks'
|
|
29
32
|
require_relative 'devbench/sidekiq_hooks'
|
|
33
|
+
require_relative 'devbench/logs'
|
|
34
|
+
require_relative 'devbench/view_helper'
|
|
30
35
|
|
|
31
36
|
# The pre-0.5 name. The same module object, not a copy: ADT::Middleware *is*
|
|
32
37
|
# Devbench::Middleware, so a stack holding either holds the one class.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'devbench'
|
|
5
|
+
|
|
6
|
+
module Devbench
|
|
7
|
+
# bin/rails generate devbench
|
|
8
|
+
#
|
|
9
|
+
# Step 2 of the install (DECISIONS #161):
|
|
10
|
+
#
|
|
11
|
+
# 1. bundle add devbench
|
|
12
|
+
# 2. bin/rails generate devbench
|
|
13
|
+
# 3. set DEVBENCH_DSN
|
|
14
|
+
#
|
|
15
|
+
# Does two things, and touches nothing else:
|
|
16
|
+
#
|
|
17
|
+
# * puts <%= devbench_script_tag %> in the application layout's <head>
|
|
18
|
+
# (.erb, .haml or .slim), or prints exactly what to add where;
|
|
19
|
+
# * if config/initializers/content_security_policy.rb defines a policy,
|
|
20
|
+
# allows the sensor's script (https://unpkg.com) in script_src, and
|
|
21
|
+
# the ingest host plus evidence storage (https://*.storage.supabase.co)
|
|
22
|
+
# in connect_src.
|
|
23
|
+
#
|
|
24
|
+
# Idempotent: a second run changes nothing. Prints what it changed.
|
|
25
|
+
class DevbenchGenerator < ::Rails::Generators::Base
|
|
26
|
+
namespace 'devbench'
|
|
27
|
+
desc 'Adds the Dev Bench browser tag to your layout and allows it in your CSP.'
|
|
28
|
+
|
|
29
|
+
TAG = 'devbench_script_tag'
|
|
30
|
+
SCRIPT_HOST = 'https://unpkg.com'
|
|
31
|
+
STORAGE_HOST = 'https://*.storage.supabase.co'
|
|
32
|
+
# Ruby code, not a host: evaluated when the CSP initializer runs, so the
|
|
33
|
+
# policy follows DEVBENCH_DSN (Devbench.csp_connect_sources).
|
|
34
|
+
RUNTIME_CONNECT = '*Devbench.csp_connect_sources'
|
|
35
|
+
LAYOUTS = %w[erb haml slim].map { |ext| "app/views/layouts/application.html.#{ext}" }.freeze
|
|
36
|
+
CSP = 'config/initializers/content_security_policy.rb'
|
|
37
|
+
|
|
38
|
+
def add_script_tag
|
|
39
|
+
path = LAYOUTS.find { |p| File.exist?(File.join(destination_root, p)) }
|
|
40
|
+
return manual_tag('no app/views/layouts/application.html.{erb,haml,slim} found') if path.nil?
|
|
41
|
+
|
|
42
|
+
full = File.join(destination_root, path)
|
|
43
|
+
text = File.read(full)
|
|
44
|
+
if text.include?(TAG)
|
|
45
|
+
say_status :identical, "#{path} (already has #{TAG})", :blue
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
updated = path.end_with?('.erb') ? insert_erb(text) : insert_indented(text, path.end_with?('.haml') ? /%head\b/ : /head\b/)
|
|
50
|
+
return manual_tag("could not find the <head> of #{path}") if updated.nil?
|
|
51
|
+
|
|
52
|
+
File.write(full, updated)
|
|
53
|
+
say_status :insert, "#{path}: <%= #{TAG} %> in <head>", :green
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def allow_in_content_security_policy
|
|
57
|
+
full = File.join(destination_root, CSP)
|
|
58
|
+
unless File.exist?(full)
|
|
59
|
+
say_status :skip, "#{CSP} not found: no CSP to change", :blue
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
text = File.read(full)
|
|
64
|
+
var = text[/^[ \t]*(?:Rails\.application\.)?config\.content_security_policy[ \t]+do[ \t]*\|[ \t]*(\w+)[ \t]*\|/, 1]
|
|
65
|
+
if var.nil?
|
|
66
|
+
say_status :skip, "#{CSP} defines no policy (all commented out): nothing to change", :blue
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
changes = []
|
|
71
|
+
text = allow(text, var, 'script_src', [SCRIPT_HOST], changes)
|
|
72
|
+
text = allow(text, var, 'connect_src', [RUNTIME_CONNECT], changes)
|
|
73
|
+
if changes.empty?
|
|
74
|
+
say_status :identical, "#{CSP} (already allows Dev Bench)", :blue
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
File.write(full, text)
|
|
79
|
+
changes.each { |c| say_status :csp, "#{CSP}: #{c}", :green }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def print_next_steps
|
|
83
|
+
say ''
|
|
84
|
+
say 'Next:'
|
|
85
|
+
say ' 1. Set DEVBENCH_DSN in this app\'s environment (the value `adt dsn create` printed).'
|
|
86
|
+
say ' 2. Check it: bin/rails devbench:test'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def manual_tag(reason)
|
|
92
|
+
say_status :manual, reason, :yellow
|
|
93
|
+
say " Add this to your layout, just before </head>:\n\n <%= #{TAG} %>\n\n" \
|
|
94
|
+
" (Haml or Slim: `= #{TAG}` as the last line inside head.)"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Before </head>, indented one step deeper than it when it is on a line
|
|
98
|
+
# of its own.
|
|
99
|
+
def insert_erb(text)
|
|
100
|
+
at = text =~ %r{</head\s*>}i
|
|
101
|
+
return nil if at.nil?
|
|
102
|
+
|
|
103
|
+
line_start = text.rindex("\n", at - 1)
|
|
104
|
+
line_start = line_start.nil? ? 0 : line_start + 1
|
|
105
|
+
before = text[line_start...at]
|
|
106
|
+
if before.strip.empty?
|
|
107
|
+
"#{text[0...line_start]}#{before} <%= #{TAG} %>\n#{text[line_start..]}"
|
|
108
|
+
else
|
|
109
|
+
"#{text[0...at]}<%= #{TAG} %>#{text[at..]}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Haml / Slim: `= devbench_script_tag` as the last child of the head
|
|
114
|
+
# element, at its children's indentation.
|
|
115
|
+
def insert_indented(text, head)
|
|
116
|
+
lines = text.lines
|
|
117
|
+
index = lines.index { |l| l =~ /\A([ \t]*)#{head}/ }
|
|
118
|
+
return nil if index.nil?
|
|
119
|
+
|
|
120
|
+
head_indent = lines[index][/\A[ \t]*/]
|
|
121
|
+
last = index
|
|
122
|
+
child_indent = nil
|
|
123
|
+
(index + 1...lines.length).each do |i|
|
|
124
|
+
line = lines[i]
|
|
125
|
+
next if line.strip.empty?
|
|
126
|
+
|
|
127
|
+
indent = line[/\A[ \t]*/]
|
|
128
|
+
break if indent.length <= head_indent.length
|
|
129
|
+
|
|
130
|
+
child_indent ||= indent
|
|
131
|
+
last = i
|
|
132
|
+
end
|
|
133
|
+
child_indent ||= "#{head_indent} "
|
|
134
|
+
lines[last] = "#{lines[last]}\n" unless lines[last].end_with?("\n")
|
|
135
|
+
lines.insert(last + 1, "#{child_indent}= #{TAG}\n")
|
|
136
|
+
lines.join
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Adds each host to `var.directive`, or — when the directive is absent
|
|
140
|
+
# but default_src is set — adds the directive as default_src's sources
|
|
141
|
+
# plus the hosts, which is what the browser was falling back to. Leaves
|
|
142
|
+
# a policy that restricts neither alone.
|
|
143
|
+
def allow(text, var, directive, hosts, changes)
|
|
144
|
+
lines = text.lines
|
|
145
|
+
range = statement(lines, var, directive)
|
|
146
|
+
if range
|
|
147
|
+
hosts.each do |host|
|
|
148
|
+
stmt = lines[range].join
|
|
149
|
+
next if stmt.include?(host)
|
|
150
|
+
|
|
151
|
+
lines[range.last] = append_source(lines[range.last], host)
|
|
152
|
+
changes << "added #{host} to #{directive}"
|
|
153
|
+
end
|
|
154
|
+
return lines.join
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Neither: the policy does not restrict this kind of request at all.
|
|
158
|
+
default = statement(lines, var, 'default_src')
|
|
159
|
+
return text if default.nil?
|
|
160
|
+
|
|
161
|
+
first = lines[default.first]
|
|
162
|
+
indent = first[/\A[ \t]*/]
|
|
163
|
+
sources = lines[default].map { |l| strip_comment(l.chomp) }.join(' ')
|
|
164
|
+
.sub(/\A[ \t]*#{var}\.default_src/, '').gsub(/\s+/, ' ').strip
|
|
165
|
+
new_line = "#{indent}#{var}.#{directive} #{[sources, *hosts.map { |h| source_code(h) }].reject(&:empty?).join(', ')}\n"
|
|
166
|
+
lines.insert(default.last + 1, new_line)
|
|
167
|
+
changes << "added #{directive} (default_src's sources + #{hosts.join(', ')})"
|
|
168
|
+
lines.join
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The line range of an uncommented `var.directive ...` statement,
|
|
172
|
+
# following continuation lines (a trailing comma or backslash).
|
|
173
|
+
def statement(lines, var, directive)
|
|
174
|
+
start = lines.index { |l| l =~ /\A[ \t]*#{Regexp.escape(var)}\.#{directive}\b/ }
|
|
175
|
+
return nil if start.nil?
|
|
176
|
+
|
|
177
|
+
last = start
|
|
178
|
+
last += 1 while last + 1 < lines.length && strip_comment(lines[last]).rstrip.end_with?(',', '\\')
|
|
179
|
+
start..last
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def append_source(line, host)
|
|
183
|
+
body = line.chomp
|
|
184
|
+
newline = line.end_with?("\n") ? "\n" : ''
|
|
185
|
+
code = strip_comment(body).rstrip
|
|
186
|
+
comment = body[code.length..]
|
|
187
|
+
"#{code}, #{source_code(host)}#{comment}#{newline}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# A trailing `# comment`, not counting '#' inside a string or #{}.
|
|
191
|
+
def strip_comment(line)
|
|
192
|
+
quote = nil
|
|
193
|
+
line.each_char.with_index do |ch, i|
|
|
194
|
+
if quote
|
|
195
|
+
quote = nil if ch == quote && line[i - 1] != '\\'
|
|
196
|
+
elsif ch == '"' || ch == "'"
|
|
197
|
+
quote = ch
|
|
198
|
+
elsif ch == '#' && line[i + 1] != '{'
|
|
199
|
+
return line[0...i]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
line
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# A host is written as a string literal; a runtime splat as code.
|
|
206
|
+
def source_code(source)
|
|
207
|
+
source.start_with?('*') ? source : source.inspect
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devbench
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dev Bench
|
|
@@ -15,7 +15,9 @@ description: |
|
|
|
15
15
|
Sidekiq jobs, and failures the application handled, with who was
|
|
16
16
|
affected, to Dev Bench. Fingerprints and counts in-process and sends one
|
|
17
17
|
small request a minute; detail is uploaded only when Dev Bench asks for
|
|
18
|
-
it, scrubbed first.
|
|
18
|
+
it, scrubbed first. Keeps recent server log lines per user action for
|
|
19
|
+
triage, renders the browser sensor's tag, and hooks itself into Rails:
|
|
20
|
+
bundle add devbench, bin/rails generate devbench, set DEVBENCH_DSN.
|
|
19
21
|
|
|
20
22
|
Adds no dependencies beyond the standard library, never alters a response
|
|
21
23
|
body, and never raises: a diagnostics gem that can fail a request is worse
|
|
@@ -32,9 +34,11 @@ files:
|
|
|
32
34
|
- lib/devbench/config.rb
|
|
33
35
|
- lib/devbench/current.rb
|
|
34
36
|
- lib/devbench/direct.rb
|
|
37
|
+
- lib/devbench/egress_policy.rb
|
|
35
38
|
- lib/devbench/fingerprint.rb
|
|
36
39
|
- lib/devbench/http.rb
|
|
37
40
|
- lib/devbench/identity.rb
|
|
41
|
+
- lib/devbench/logs.rb
|
|
38
42
|
- lib/devbench/middleware.rb
|
|
39
43
|
- lib/devbench/rails_hooks.rb
|
|
40
44
|
- lib/devbench/railtie.rb
|
|
@@ -47,6 +51,8 @@ files:
|
|
|
47
51
|
- lib/devbench/trace.rb
|
|
48
52
|
- lib/devbench/transport.rb
|
|
49
53
|
- lib/devbench/version.rb
|
|
54
|
+
- lib/devbench/view_helper.rb
|
|
55
|
+
- lib/generators/devbench/devbench_generator.rb
|
|
50
56
|
homepage: https://github.com/pasperry/devbench-sdk
|
|
51
57
|
licenses:
|
|
52
58
|
- LicenseRef-Proprietary
|