tinymonrb 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c33d3e807a92d0f19e975e8d384e2a6a9e78ed22e62c4f2fdf5a5c2e3ff953c7
4
- data.tar.gz: 667a99ab8a3b6d3e08780c56e99ccbadbf0c748cd3c2524dfd8ecc78e8911b6b
3
+ metadata.gz: 8df6c08b32ccae44e241b41fbd2a98f2033043c37ce6292a2f529869838cb8aa
4
+ data.tar.gz: 8e924831169fbbbbd9c409c49c10a71428eef4dbd48e9b01345f606230e75eb5
5
5
  SHA512:
6
- metadata.gz: '085fdbe62bc6c876ee0c15d40de22f7645141064d7f12cd9035789d2684bfcf3f19f7c1eeb0e0a830311df6eaaa219d3018997b0a0fb96576147a63fe0cd455d'
7
- data.tar.gz: 13543ba9734ac10fc3a9ccda8b9c456d7d39e57314449f7583ab4dc48572c9d40051d0bd8f4cfe62d3660f74b23b3dcef5834fac20a464c709685eefd2f9af78
6
+ metadata.gz: 27265f4b71e845ffba1f87f3679f6979e7ec5a0175cdbc3399130a2140a8896178554ce72504159f1469674e288ef6a959f0dc84c293ff20a8a4f6b89cf55345
7
+ data.tar.gz: 0ff34d5f4fa892fb1dc00076ba17746836fd5d2205a4b804ca3cf16ec333e81fb307cdcabbbf0a1b778e1ae8cd8a8ff8392e1affaae381def405c72953af09d1
@@ -2,37 +2,41 @@
2
2
 
3
3
  require_relative "event_builder"
4
4
  require_relative "scope"
5
+ require_relative "scrub"
5
6
  require_relative "transport"
6
7
 
7
8
  module Tinymon
8
9
  class Client
9
10
  DEFAULT_ENDPOINT = "https://console.tinymon.dev/api/ingest"
10
11
 
11
- def initialize(dsn:, endpoint: nil, environment: nil, release: nil, sample_rate: 1.0, before_send: nil)
12
+ def initialize(
13
+ dsn:,
14
+ endpoint: nil,
15
+ environment: nil,
16
+ release: nil,
17
+ sample_rate: 1.0,
18
+ before_send: nil,
19
+ # Privacy controls — defaults are privacy-preserving.
20
+ scrub_url_query: true,
21
+ scrub_patterns: nil,
22
+ send_default_pii: false
23
+ )
12
24
  @dsn = dsn
13
25
  @endpoint = endpoint || DEFAULT_ENDPOINT
14
26
  @environment = environment
15
27
  @release = release
16
28
  @sample_rate = sample_rate
17
29
  @before_send = before_send
30
+ @scrub_url_query = scrub_url_query
31
+ @scrub_patterns = scrub_patterns
32
+ @send_default_pii = send_default_pii
18
33
  @transport = Transport.new(@endpoint, dsn)
19
34
  end
20
35
 
21
36
  def capture_exception(exception)
22
37
  return if rand > @sample_rate
23
- snap = SCOPE.snapshot
24
- event = EventBuilder.build(
25
- exception,
26
- release: @release,
27
- environment: @environment,
28
- user: snap[:user],
29
- tags: snap[:tags],
30
- breadcrumbs: snap[:breadcrumbs],
31
- )
32
- if @before_send
33
- event = @before_send.call(event)
34
- return if event.nil?
35
- end
38
+ event = _prepare(exception)
39
+ return if event.nil?
36
40
  @transport.enqueue(event)
37
41
  rescue StandardError
38
42
  # SWALLOW. The SDK must never throw into the host app.
@@ -40,21 +44,50 @@ module Tinymon
40
44
  end
41
45
 
42
46
  def capture_message(message, level: "info")
43
- synthetic = StandardError.new(message)
47
+ event = _prepare(StandardError.new(message))
48
+ return if event.nil?
49
+ event["level"] = level
50
+ event["exception"]["type"] = "Message"
51
+ @transport.enqueue(event)
52
+ rescue StandardError
53
+ nil
54
+ end
55
+
56
+ # Per-event context capture. Use from Rack middleware; do NOT use
57
+ # Tinymon.set_user/set_tag for per-request data — they race under
58
+ # concurrency.
59
+ def capture_exception_with_context(exception, request: nil, user: nil, tags: nil)
60
+ return if rand > @sample_rate
61
+ event = _prepare(exception, ctx: { request: request, user: user, tags: tags })
62
+ return if event.nil?
63
+ @transport.enqueue(event)
64
+ rescue StandardError
65
+ nil
66
+ end
67
+
68
+ private
69
+
70
+ def _prepare(exception, ctx: nil)
44
71
  snap = SCOPE.snapshot
72
+ ctx ||= {}
45
73
  event = EventBuilder.build(
46
- synthetic,
74
+ exception,
47
75
  release: @release,
48
76
  environment: @environment,
49
- user: snap[:user],
50
- tags: snap[:tags],
77
+ user: ctx[:user] || snap[:user],
78
+ tags: ctx[:tags] || snap[:tags],
51
79
  breadcrumbs: snap[:breadcrumbs],
80
+ request: ctx[:request],
52
81
  )
53
- event["level"] = level
54
- event["exception"]["type"] = "Message"
55
- @transport.enqueue(event)
56
- rescue StandardError
57
- nil
82
+ # Default scrub BEFORE before_send.
83
+ Scrub.scrub_event(event, scrub_url_query: @scrub_url_query, scrub_patterns: @scrub_patterns)
84
+ # PII gate on the wire — server only attaches IP if true.
85
+ event["send_default_pii"] = !!@send_default_pii
86
+ if @before_send
87
+ event = @before_send.call(event)
88
+ return nil if event.nil?
89
+ end
90
+ event
58
91
  end
59
92
  end
60
93
  end
@@ -12,7 +12,7 @@ module Tinymon
12
12
 
13
13
  SENSITIVE = /password|token|secret|auth|card|cvv|ssn/i.freeze
14
14
 
15
- def build(exception, release: nil, environment: nil, user: nil, tags: nil, breadcrumbs: nil, url: nil)
15
+ def build(exception, release: nil, environment: nil, user: nil, tags: nil, breadcrumbs: nil, url: nil, request: nil)
16
16
  event = {
17
17
  "event_id" => SecureRandom.uuid,
18
18
  "timestamp" => Time.now.to_f,
@@ -30,7 +30,16 @@ module Tinymon
30
30
  }
31
31
  event["release"] = release if release
32
32
  event["environment"] = environment if environment
33
- event["request"] = { "url" => url } if url
33
+ if request && (request[:method] || request["method"] || request[:url] || request["url"])
34
+ req = {}
35
+ m = request[:method] || request["method"]
36
+ u = request[:url] || request["url"]
37
+ req["method"] = m if m
38
+ req["url"] = u if u
39
+ event["request"] = req
40
+ elsif url
41
+ event["request"] = { "url" => url }
42
+ end
34
43
  event
35
44
  end
36
45
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rack middleware. Insert into ANY Rack app (Rails, Sinatra, Hanami,
4
+ # plain Rack) to capture in-request exceptions. Captures with per-event
5
+ # request context (method + URL) and RE-RAISES so the framework's own
6
+ # error handling still fires.
7
+ #
8
+ # Manual insert:
9
+ # require "tinymonrb"
10
+ # use Tinymon::Rack # Sinatra / plain Rack
11
+ # # Rails:
12
+ # config.middleware.use Tinymon::Rack
13
+ #
14
+ # Or use the Railtie for auto-insert in Rails apps (loaded automatically
15
+ # when Rails is detected at runtime).
16
+
17
+ module Tinymon
18
+ class Rack
19
+ def initialize(app)
20
+ @app = app
21
+ end
22
+
23
+ def call(env)
24
+ @app.call(env)
25
+ rescue StandardError, ScriptError => e
26
+ capture(e, env)
27
+ raise
28
+ end
29
+
30
+ private
31
+
32
+ def capture(exc, env)
33
+ mod = ::Tinymon
34
+ client = mod.instance_variable_get(:@client)
35
+ return unless client
36
+
37
+ url = build_url(env)
38
+ method = env["REQUEST_METHOD"]
39
+ client.capture_exception_with_context(
40
+ exc,
41
+ request: { method: method, url: url }
42
+ )
43
+ rescue StandardError
44
+ # never let the SDK break the request
45
+ nil
46
+ end
47
+
48
+ def build_url(env)
49
+ scheme = env["rack.url_scheme"] || "http"
50
+ host = env["HTTP_HOST"] || env["SERVER_NAME"] || ""
51
+ path = env["REQUEST_URI"] ||
52
+ "#{env['SCRIPT_NAME']}#{env['PATH_INFO']}#{env['QUERY_STRING'] && !env['QUERY_STRING'].empty? ? "?#{env['QUERY_STRING']}" : ''}"
53
+ host.empty? ? path : "#{scheme}://#{host}#{path}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Auto-insert Tinymon::Rack into the Rails middleware stack. Loaded only
4
+ # when Rails is present at require time — no hard Rails dependency.
5
+ require_relative "rack"
6
+
7
+ module Tinymon
8
+ class Railtie < ::Rails::Railtie
9
+ initializer "tinymon.middleware" do |app|
10
+ app.middleware.use Tinymon::Rack
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Default PII scrub. Identical semantics to the JS SDK and the server.
4
+ # Runs BEFORE before_send so user hooks can both relax and tighten the defaults.
5
+ module Tinymon
6
+ module Scrub
7
+ SENSITIVE_TAG_KEY = /password|token|secret|auth|card|cvv|ssn/i.freeze
8
+
9
+ DEFAULT_PATTERNS = [
10
+ /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/, # email
11
+ /\b(?:\d[ \-]?){12,15}\d\b/, # card-like
12
+ /\b[Bb]earer\s+[A-Za-z0-9._\-]+/, # bearer
13
+ /\bAuthorization\s*:\s*\S+/ # auth header
14
+ ].freeze
15
+
16
+ module_function
17
+
18
+ def scrub_string(s, patterns)
19
+ out = s.to_s.dup
20
+ patterns.each { |re| out.gsub!(re, "[redacted]") }
21
+ out
22
+ end
23
+
24
+ def strip_url_query(url)
25
+ return url if url.nil? || url.empty?
26
+ i = url.index(/[?#]/)
27
+ i.nil? ? url : url[0...i]
28
+ end
29
+
30
+ # Mutate the event hash in place. Returns it for chaining.
31
+ def scrub_event(event, scrub_url_query: true, scrub_patterns: nil)
32
+ patterns = DEFAULT_PATTERNS + Array(scrub_patterns)
33
+
34
+ if scrub_url_query && (req = event[:request] || event["request"])
35
+ url_key = req.key?(:url) ? :url : "url"
36
+ req[url_key] = strip_url_query(req[url_key]) if req[url_key]
37
+ end
38
+
39
+ if (exc = event[:exception] || event["exception"])
40
+ val_key = exc.key?(:value) ? :value : "value"
41
+ exc[val_key] = scrub_string(exc[val_key], patterns) if exc[val_key]
42
+ end
43
+
44
+ Array(event[:breadcrumbs] || event["breadcrumbs"]).each do |b|
45
+ msg_key = b.key?(:message) ? :message : "message"
46
+ b[msg_key] = scrub_string(b[msg_key], patterns) if b[msg_key]
47
+ end
48
+
49
+ tags = event[:tags] || event["tags"]
50
+ if tags
51
+ cleaned = {}
52
+ tags.each { |k, v| cleaned[k] = SENSITIVE_TAG_KEY.match?(k.to_s) ? "[redacted]" : v }
53
+ if event.key?(:tags)
54
+ event[:tags] = cleaned
55
+ else
56
+ event["tags"] = cleaned
57
+ end
58
+ end
59
+
60
+ event
61
+ end
62
+ end
63
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tinymon
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  SDK_NAME = "tinymon.ruby"
6
6
  end
data/lib/tinymonrb.rb CHANGED
@@ -7,6 +7,9 @@ require_relative "tinymon/scope"
7
7
  require_relative "tinymon/transport"
8
8
  require_relative "tinymon/client"
9
9
  require_relative "tinymon/integrations"
10
+ require_relative "tinymon/rack"
11
+ # Rails auto-insert — only loaded when Rails is present at require time.
12
+ require_relative "tinymon/railtie" if defined?(::Rails::Railtie)
10
13
 
11
14
  # Public API:
12
15
  #
@@ -23,7 +26,17 @@ module Tinymon
23
26
  @client = nil
24
27
 
25
28
  class << self
26
- def init(dsn:, endpoint: nil, environment: nil, release: nil, sample_rate: 1.0, before_send: nil)
29
+ def init(
30
+ dsn:,
31
+ endpoint: nil,
32
+ environment: nil,
33
+ release: nil,
34
+ sample_rate: 1.0,
35
+ before_send: nil,
36
+ scrub_url_query: true,
37
+ scrub_patterns: nil,
38
+ send_default_pii: false
39
+ )
27
40
  @client = Client.new(
28
41
  dsn: dsn,
29
42
  endpoint: endpoint,
@@ -31,6 +44,9 @@ module Tinymon
31
44
  release: release,
32
45
  sample_rate: sample_rate,
33
46
  before_send: before_send,
47
+ scrub_url_query: scrub_url_query,
48
+ scrub_patterns: scrub_patterns,
49
+ send_default_pii: send_default_pii,
34
50
  )
35
51
  Integrations.install(@client)
36
52
  @client
@@ -40,6 +56,12 @@ module Tinymon
40
56
  @client&.capture_exception(exception)
41
57
  end
42
58
 
59
+ # Per-event context capture. Use from Rack middleware; do NOT use
60
+ # set_user/set_tag for per-request data (they race under concurrency).
61
+ def capture_exception_with_context(exception, request: nil, user: nil, tags: nil)
62
+ @client&.capture_exception_with_context(exception, request: request, user: user, tags: tags)
63
+ end
64
+
43
65
  def capture_message(message, level: "info")
44
66
  @client&.capture_message(message, level: level)
45
67
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tinymonrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - tinymon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-06 00:00:00.000000000 Z
11
+ date: 2026-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json_schemer
@@ -48,7 +48,10 @@ files:
48
48
  - lib/tinymon/client.rb
49
49
  - lib/tinymon/event_builder.rb
50
50
  - lib/tinymon/integrations.rb
51
+ - lib/tinymon/rack.rb
52
+ - lib/tinymon/railtie.rb
51
53
  - lib/tinymon/scope.rb
54
+ - lib/tinymon/scrub.rb
52
55
  - lib/tinymon/stacktrace.rb
53
56
  - lib/tinymon/transport.rb
54
57
  - lib/tinymon/version.rb