forge_ops_tracker 0.2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 461cec25deb5d055e670ab0bf6e9fbc3cc7183fee4b9e7e731cbefabc4c615f3
4
- data.tar.gz: d7fe09d86a29c7ecca952c0f9ee5bfaa839bdf8541c1d787e7b752e599c33347
3
+ metadata.gz: f15d33570709adad8c061ee0c432d9a6484fc50881076fae1493b892562188e7
4
+ data.tar.gz: f5fada725654a4006a6a44ee0680fb5f7f34cb808087305596892101c743a0ef
5
5
  SHA512:
6
- metadata.gz: 50c03a3c07c566d8a665fcf79996ce41b4f5012e35bca1e369da9b1d1de71ebda6d429d25e025a3f15054f93287406d5656dd7e54ac4f6bc8b0ca6c6fa2497cb
7
- data.tar.gz: 2ecb9ce97ec67a22b3f21d3e6945bdc3bd45f2c7199fd92834756d9081c3ccc25694d8226933a6c72de8f0efc4be4658dfe1061beba36c3998e45c181cba2f37
6
+ metadata.gz: 66513f4aebaeb443198a73bad548faaab86653841d213c37000bd343de406926053faa58b65be68934d90c8e12cc1393061ee763e7b5541c4545e90713910fb9
7
+ data.tar.gz: 86eb61aca41a42a55a6df37df0d8ef7789b5342055239640a593310da438cda20e3d6e959a0b229c4dbcbe137089e31d8bd537b1eb8d4e91c72bec4acfd6ec8c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - Every reported event now carries `sdk_name` ("ruby"), so a project's language on the ForgeOps
6
+ dashboard auto-detects from whichever SDK is actually reporting into it, rather than staying
7
+ stuck on whatever it was created with. Never overrides a language picked by hand in project
8
+ settings.
9
+
10
+ ## 0.4.0
11
+
12
+ - Session tracking (release health): every request is now counted as a session, crash-free unless
13
+ an unhandled exception actually escapes it, so the ForgeOps dashboard can show a crash-free rate
14
+ per release rather than only ever hearing about the requests that broke. Counted in-process and
15
+ flushed as a small periodic aggregate (`config.session_flush_interval`, default 60 seconds) on
16
+ its own background thread, not one network call per request. On by default, the same as error
17
+ tracking itself (`config.track_sessions = false` opts out); requires a ForgeOps plan that
18
+ includes release health, checked server-side on every flush.
19
+
20
+ ## 0.3.0
21
+
22
+ - Source context capture: each in-app backtrace frame can now carry the 5 lines of source on
23
+ either side of the culprit line, read straight off disk at raise-time, so an issue's detail page
24
+ can show the actual code that broke, not just a file:line:method reference. On by default
25
+ (`config.capture_source_context = false` opts out), but the real, durable protection is a
26
+ per-project setting on the ForgeOps server, not this flag: turning it off there stops the server
27
+ from ever storing captured source lines for that project again, regardless of what any individual
28
+ app's own local setting is still set to. Never applies to a frame outside your app's own code,
29
+ and fails silently for any file that can't be read.
30
+
3
31
  ## 0.2.0
4
32
 
5
33
  - Client-side PII scrubbing: payloads are scrubbed (email addresses, formatted SSNs/credit
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ForgeOpsTracker
2
2
 
3
- Rails exception reporting client for a private, self-hosted [ForgeOps](../../) tracker instance.
3
+ Rails exception reporting client for a [ForgeOps](../../) instance.
4
4
 
5
5
  ## Installation
6
6
 
@@ -22,17 +22,17 @@ end
22
22
  ```
23
23
 
24
24
  Delivery happens on a small background thread with a bounded queue and short HTTP timeouts. Every
25
- failure mode -- network errors, timeouts, a full queue, a malformed DSN -- is caught and dropped
25
+ failure mode (network errors, timeouts, a full queue, a malformed DSN) is caught and dropped
26
26
  rather than raised, so a broken or unreachable tracker can never take down the host app.
27
27
 
28
28
  ## What gets reported automatically, and what doesn't
29
29
 
30
30
  **Unhandled exceptions need no further wiring at all.** The gem's Railtie subscribes to
31
31
  `Rails.error` automatically, and Rails itself reports anything that crashes a request or job
32
- through that same channel -- install the gem, set a DSN, and those show up in ForgeOps with zero
32
+ through that same channel: install the gem, set a DSN, and those show up in ForgeOps with zero
33
33
  other code changes.
34
34
 
35
- **Handled exceptions -- code that catches its own error to keep running -- are a different story.**
35
+ **Handled exceptions (code that catches its own error to keep running) are a different story.**
36
36
  A plain `rescue` the gem never hears about, no matter what:
37
37
 
38
38
  ```ruby
@@ -40,19 +40,19 @@ begin
40
40
  charge_card(order)
41
41
  rescue Stripe::CardError => e
42
42
  logger.warn("card declined: #{e.message}")
43
- # ForgeOps never sees this -- nothing here goes through Rails.error at all.
43
+ # ForgeOps never sees this: nothing here goes through Rails.error at all.
44
44
  end
45
45
  ```
46
46
 
47
47
  To report it *and* keep swallowing it, swap the bare `rescue` for Rails' own built-in
48
- `Rails.error.handle` -- this isn't a ForgeOps-specific API, it's Rails' own error-reporting
48
+ `Rails.error.handle`: this isn't a ForgeOps-specific API, it's Rails' own error-reporting
49
49
  convention (Rails 7+), which the gem just happens to already be subscribed to:
50
50
 
51
51
  ```ruby
52
52
  Rails.error.handle(fallback: -> { nil }) do
53
53
  charge_card(order)
54
54
  end
55
- # Reported to every Rails.error subscriber, including this gem, then swallowed --
55
+ # Reported to every Rails.error subscriber, including this gem, then swallowed:
56
56
  # execution continues past the block either way.
57
57
  ```
58
58
 
@@ -70,8 +70,8 @@ code already catches and handles it, route that specific `rescue` through `Rails
70
70
  ## PII scrubbing
71
71
 
72
72
  By default, the message, backtrace, and any context/tags you attach are scanned for likely
73
- personal data -- email addresses, formatted SSNs/credit cards, known API key/token formats, and
74
- anything under a suspiciously-named key (`password`, `api_key`, `ssn`, and similar) -- and redacted
73
+ personal data: email addresses, formatted SSNs/credit cards, known API key/token formats, and
74
+ anything under a suspiciously-named key (`password`, `api_key`, `ssn`, and similar); redacted
75
75
  before the payload ever leaves this process. ForgeOps itself scrubs again on arrival regardless, so
76
76
  this is a second, earlier layer, not the only one.
77
77
 
@@ -83,3 +83,44 @@ ForgeOpsTracker.configure do |config|
83
83
  config.scrub_pii = false
84
84
  end
85
85
  ```
86
+
87
+ ## Source context
88
+
89
+ By default, each in-app backtrace frame (never a vendored gem) is captured along with the 5 lines
90
+ of source on either side of the culprit line, read straight off disk at raise-time, so an issue's
91
+ detail page can show the actual code that broke, not just a `file:line:method` reference. This
92
+ never applies to a frame outside `app_root`, and it fails silently (no context, not an error) for
93
+ any file that can't be read for whatever reason.
94
+
95
+ This is a real, deliberate exception to "off by default is safer": literal source code is being
96
+ transmitted, not just a reference to it, and the real protection here is not this flag. Every
97
+ project on ForgeOps has its own setting (on by default, off durably and immediately once an org
98
+ owner turns it off, regardless of what any individual app's own `config.capture_source_context` is
99
+ still set to) that governs whether the server will ever actually store what an SDK sends; see the
100
+ in-app help docs. Use this flag if you'd rather this gem never even attempt the disk read in the
101
+ first place:
102
+
103
+ ```ruby
104
+ ForgeOpsTracker.configure do |config|
105
+ config.capture_source_context = false
106
+ end
107
+ ```
108
+
109
+ ## Session tracking (release health)
110
+
111
+ By default, every request is counted as a session: crash-free unless an unhandled exception
112
+ actually escapes it, giving ForgeOps a crash-free rate per release to show alongside the errors
113
+ themselves, not just the errors on their own. Counted in-process and flushed as a small periodic
114
+ aggregate on a background thread (never one network call per request), the same delivery
115
+ philosophy as everything else in this gem: a broken or unreachable tracker never affects the host
116
+ app either way.
117
+
118
+ ```ruby
119
+ ForgeOpsTracker.configure do |config|
120
+ config.track_sessions = false # opt out entirely
121
+ config.session_flush_interval = 30 # seconds; default 60
122
+ end
123
+ ```
124
+
125
+ Requires a ForgeOps plan that includes release health; on a plan that doesn't, the periodic
126
+ flushes are simply rejected server-side and dropped, exactly like any other delivery failure.
@@ -3,8 +3,8 @@ require "json"
3
3
  require "uri"
4
4
 
5
5
  module ForgeOpsTracker
6
- # Delivers one payload over HTTP. Every failure mode -- DNS, connection,
7
- # timeout, TLS, a non-2xx response -- is caught here and turned into a
6
+ # Delivers one payload over HTTP. Every failure mode; DNS, connection,
7
+ # timeout, TLS, a non-2xx response; is caught here and turned into a
8
8
  # `false` return rather than a raised exception, since a broken or
9
9
  # unreachable tracker must never be able to break the host app.
10
10
  class Client
@@ -13,19 +13,29 @@ module ForgeOpsTracker
13
13
  end
14
14
 
15
15
  def deliver(payload)
16
- uri = configuration.ingestion_uri
17
- return false unless uri
18
-
19
- response = http_for(uri).request(build_request(uri, payload))
20
- response.is_a?(Net::HTTPSuccess)
21
- rescue StandardError => e
22
- log { "delivery failed: #{e.class}: #{e.message}" }
23
- false
16
+ post(configuration.ingestion_uri, payload)
17
+ end
18
+
19
+ # Same delivery contract as deliver above (never raises, returns a bare
20
+ # true/false), just a different endpoint under the same DSN's host and
21
+ # api_key; see Configuration#session_checkins_uri.
22
+ def deliver_session_checkin(payload)
23
+ post(configuration.session_checkins_uri, payload)
24
24
  end
25
25
 
26
26
  private
27
27
  attr_reader :configuration
28
28
 
29
+ def post(uri, payload)
30
+ return false unless uri
31
+
32
+ response = http_for(uri).request(build_request(uri, payload))
33
+ response.is_a?(Net::HTTPSuccess)
34
+ rescue StandardError => e
35
+ log { "delivery failed: #{e.class}: #{e.message}" }
36
+ false
37
+ end
38
+
29
39
  def http_for(uri)
30
40
  http = Net::HTTP.new(uri.host, uri.port)
31
41
  http.use_ssl = uri.scheme == "https"
@@ -3,10 +3,12 @@ require "socket"
3
3
 
4
4
  module ForgeOpsTracker
5
5
  class Configuration
6
- # A single Sentry-style DSN string carries both the ingestion URL and
6
+ # A single DSN string carries both the ingestion URL and
7
7
  # the project's api_key: "https://<api_key>@host/api/v1/events".
8
8
  attr_accessor :dsn, :environment, :release, :server_name, :app_root, :logger
9
9
  attr_accessor :enabled_environments, :queue_size, :open_timeout, :read_timeout, :scrub_pii
10
+ attr_accessor :capture_source_context
11
+ attr_accessor :track_sessions, :session_flush_interval
10
12
 
11
13
  def initialize
12
14
  @dsn = ENV["FORGE_OPS_DSN"]
@@ -18,13 +20,29 @@ module ForgeOpsTracker
18
20
  @open_timeout = 2
19
21
  @read_timeout = 2
20
22
  @logger = nil
21
- # See PiiScrubber -- redacts likely-sensitive content (emails,
23
+ # See PiiScrubber; redacts likely-sensitive content (emails,
22
24
  # credit cards, known API key formats, anything under a
23
25
  # suspiciously-named key) before a payload ever leaves this
24
26
  # process. ForgeOps itself scrubs again on arrival regardless, so
25
- # this is a second, earlier layer, not the only one -- but "on" is
27
+ # this is a second, earlier layer, not the only one; but "on" is
26
28
  # the only sane default.
27
29
  @scrub_pii = true
30
+ # Whether EventBuilder reads a few lines of source off disk around
31
+ # each in-app frame's culprit line (see EventBuilder#attach_source_context).
32
+ # Defaults on so a snippet shows up without extra setup, but this flag
33
+ # alone isn't the real protection against sending proprietary source
34
+ # code somewhere it shouldn't go: ForgeOps' own per-project setting is
35
+ # the durable, server-enforced off switch, since it applies regardless
36
+ # of what this flag happens to be set to on any given deployment, and
37
+ # can't drift back on the way a local config value could. Turn this
38
+ # off here too if this host app never wants that disk read attempted
39
+ # in the first place.
40
+ @capture_source_context = true
41
+ # Auto-instruments every request the moment the gem loads, the same "on unless you turn it
42
+ # off" default error tracking itself already has; nothing else in this gem is opt-in. See
43
+ # ForgeOpsTracker::Middleware::SessionTracking for what this actually wraps.
44
+ @track_sessions = true
45
+ @session_flush_interval = 60
28
46
  end
29
47
 
30
48
  def api_key
@@ -42,6 +60,18 @@ module ForgeOpsTracker
42
60
  uri
43
61
  end
44
62
 
63
+ # Same host and api_key as ingestion_uri, a sibling path under the same DSN rather than a
64
+ # second DSN to configure; the DSN's own path is always /api/v1/events (what ForgeOps issues
65
+ # to every project), so the session-checkins endpoint is derived by substitution.
66
+ def session_checkins_uri
67
+ uri = ingestion_uri
68
+ return nil unless uri
69
+
70
+ uri = uri.dup
71
+ uri.path = uri.path.sub(%r{/events\z}, "/session_checkins")
72
+ uri
73
+ end
74
+
45
75
  def enabled?
46
76
  !blank?(dsn) && !blank?(api_key) && enabled_environments.map(&:to_s).include?(environment.to_s)
47
77
  end
@@ -17,7 +17,7 @@ module ForgeOpsTracker
17
17
  end
18
18
 
19
19
  # Enqueues a payload, dropping it silently (never blocking the caller)
20
- # if the queue is already full -- a burst of exceptions must never apply
20
+ # if the queue is already full; a burst of exceptions must never apply
21
21
  # backpressure to the host app.
22
22
  def push(payload)
23
23
  ensure_worker_started
@@ -2,13 +2,27 @@ require "time"
2
2
 
3
3
  module ForgeOpsTracker
4
4
  # Turns a raised exception into the JSON-able payload shape the ingestion
5
- # API expects. Backtrace parsing is a simple regex, not a full parser --
5
+ # API expects. Backtrace parsing is a simple regex, not a full parser:
6
6
  # good enough for standard MRI backtrace lines across Ruby versions
7
7
  # (both the older `in \`method'` and newer `in 'method'` quoting styles).
8
8
  class EventBuilder
9
9
  LINE_PATTERN = /\A(?<file>.+?):(?<line>\d+)(?::in\s+(?<method>.+))?\z/
10
10
  MAX_FRAMES = 500
11
11
 
12
+ # Identifies this gem to the server's auto language-detection on the project the event lands
13
+ # in (see Project#note_sdk_platform server-side); "ruby", matching this repo's own sdks/
14
+ # directory naming for every other language's client.
15
+ SDK_NAME = "ruby".freeze
16
+
17
+ # How many lines of source to grab on either side of the culprit line
18
+ # (see #attach_source_context), and the longest a single captured line
19
+ # is allowed to be before getting truncated; guards against a single
20
+ # pathological minified/generated line ballooning the payload. ForgeOps
21
+ # itself re-truncates on arrival too, the same "don't just trust the
22
+ # SDK" posture MAX_FRAMES already gets on the server side.
23
+ CONTEXT_LINES = 5
24
+ MAX_CONTEXT_LINE_LENGTH = 500
25
+
12
26
  def initialize(configuration)
13
27
  @configuration = configuration
14
28
  end
@@ -23,7 +37,8 @@ module ForgeOpsTracker
23
37
  release: configuration.release,
24
38
  server_name: configuration.server_name,
25
39
  context: context || {},
26
- tags: {}
40
+ tags: {},
41
+ sdk_name: SDK_NAME
27
42
  }
28
43
  scrub(payload)
29
44
  end
@@ -31,8 +46,8 @@ module ForgeOpsTracker
31
46
  private
32
47
  attr_reader :configuration
33
48
 
34
- # exception_class/occurred_at/environment/release/server_name are
35
- # left alone -- structured fields this gem or the host app sets
49
+ # exception_class/occurred_at/environment/release/server_name/sdk_name are
50
+ # left alone; structured fields this gem or the host app sets
36
51
  # deliberately, not free text an exception or its context could
37
52
  # accidentally spill sensitive data into.
38
53
  def scrub(payload)
@@ -55,12 +70,13 @@ module ForgeOpsTracker
55
70
  return nil unless match
56
71
 
57
72
  file = match[:file]
58
- {
73
+ frame = {
59
74
  file: file,
60
75
  line: match[:line].to_i,
61
76
  method: match[:method]&.gsub(/\A[`'"]|['"]\z/, ""),
62
77
  in_app: in_app?(file)
63
78
  }
79
+ attach_source_context(frame)
64
80
  end
65
81
 
66
82
  def in_app?(file)
@@ -69,5 +85,43 @@ module ForgeOpsTracker
69
85
 
70
86
  file.start_with?(root.to_s) && !file.include?("/gems/") && !file.include?("/bundle/")
71
87
  end
88
+
89
+ # Reads a few lines of source straight off disk around the culprit
90
+ # line, at raise-time, in the same running process the exception came
91
+ # from. Gated on two things: the frame has to be in-app (never a
92
+ # vendored dependency; there'd be nothing meaningful to show, and
93
+ # it's not the host app's own code to begin with), and
94
+ # configuration.capture_source_context has to be on (see
95
+ # Configuration for why it defaults to true and why ForgeOps' own
96
+ # per-project setting, not this flag, is the durable, protected way
97
+ # to turn it off). Best-effort: any file that can't be read (deleted,
98
+ # permission denied, a path that only ever existed inside a build
99
+ # step and isn't present in this deployment) just means this one
100
+ # frame gets no source context, not a raised error of its own.
101
+ def attach_source_context(frame)
102
+ return frame unless configuration.capture_source_context && frame[:in_app]
103
+
104
+ lines = File.readlines(frame[:file])
105
+ index = frame[:line] - 1
106
+ return frame unless index.between?(0, lines.length - 1)
107
+
108
+ from = [ index - CONTEXT_LINES, 0 ].max
109
+ to = [ index + CONTEXT_LINES, lines.length - 1 ].min
110
+
111
+ frame.merge(
112
+ context_line: truncate_line(lines[index]),
113
+ pre_context: lines[from...index].map { |source_line| truncate_line(source_line) },
114
+ post_context: lines[(index + 1)..to].map { |source_line| truncate_line(source_line) }
115
+ )
116
+ rescue SystemCallError, IOError
117
+ frame
118
+ end
119
+
120
+ def truncate_line(line)
121
+ line = line.chomp
122
+ return line if line.length <= MAX_CONTEXT_LINE_LENGTH
123
+
124
+ "#{line[0, MAX_CONTEXT_LINE_LENGTH]}..."
125
+ end
72
126
  end
73
127
  end
@@ -0,0 +1,38 @@
1
+ module ForgeOpsTracker
2
+ module Middleware
3
+ # Wraps every request: counts it, and separately counts it as crashed only when an exception
4
+ # actually escapes to here, never inferred from whether ErrorSubscriber happened to report
5
+ # anything, since ErrorSubscriber reports handled errors too (see Rails.error.handle), so "an
6
+ # event was reported" and "this request crashed" aren't the same signal. Always re-raises,
7
+ # so the host app's own error handling and ErrorSubscriber both run completely unaffected by
8
+ # this middleware's presence; it only ever observes, never intercepts.
9
+ class SessionTracking
10
+ def initialize(app, configuration: ForgeOpsTracker.configuration, flusher: nil)
11
+ @app = app
12
+ @configuration = configuration
13
+ @flusher = flusher
14
+ end
15
+
16
+ def call(env)
17
+ return app.call(env) unless configuration.track_sessions && configuration.enabled?
18
+
19
+ begin
20
+ response = app.call(env)
21
+ rescue StandardError
22
+ flusher_instance.record_session(crashed: true)
23
+ raise
24
+ end
25
+
26
+ flusher_instance.record_session(crashed: false)
27
+ response
28
+ end
29
+
30
+ private
31
+ attr_reader :app, :configuration
32
+
33
+ def flusher_instance
34
+ @flusher ||= SessionFlusher.new(configuration)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,11 +1,11 @@
1
1
  module ForgeOpsTracker
2
2
  # Redacts likely-sensitive content out of a payload before it ever leaves
3
- # this process -- the same patterns ForgeOps itself applies again on
3
+ # this process; the same patterns ForgeOps itself applies again on
4
4
  # arrival (defense in depth: this layer keeps the data off the wire and
5
5
  # out of any request logging in between; the server-side layer is what
6
6
  # actually protects the database, and doesn't depend on every reporting
7
7
  # app running an up-to-date version of this gem). See the main
8
- # application's PiiScrubber for the shared design rationale -- kept as a
8
+ # application's PiiScrubber for the shared design rationale; kept as a
9
9
  # separate, dependency-free implementation here rather than requiring the
10
10
  # private app's code, since this gem has to work standalone in any host
11
11
  # app regardless of what's reporting into it.
@@ -10,5 +10,9 @@ module ForgeOpsTracker
10
10
 
11
11
  Rails.error.subscribe(ForgeOpsTracker::ErrorSubscriber.new(configuration))
12
12
  end
13
+
14
+ initializer "forge_ops_tracker.track_sessions" do |app|
15
+ app.middleware.use ForgeOpsTracker::Middleware::SessionTracking, configuration: ForgeOpsTracker.configuration
16
+ end
13
17
  end
14
18
  end
@@ -0,0 +1,85 @@
1
+ require "thread"
2
+
3
+ module ForgeOpsTracker
4
+ # Counts requests and crashes in-process (see Middleware::SessionTracking) and periodically
5
+ # flushes the totals as one small aggregate report, rather than one network call per request.
6
+ # Same "lazily start a background thread on first use, not at load time" pattern DeliveryQueue
7
+ # already uses, so each forked Puma/Passenger worker gets its own fresh thread instead of
8
+ # inheriting a dead one across fork; here it's triggered by a sleep-loop timer instead of a
9
+ # queue push, since there's no per-event signal to wait on, just a clock.
10
+ class SessionFlusher
11
+ def initialize(configuration, client: Client.new(configuration))
12
+ @configuration = configuration
13
+ @client = client
14
+ @mutex = Mutex.new
15
+ @sessions_count = 0
16
+ @crashed_sessions_count = 0
17
+ @period_started_at = Time.now.utc
18
+ @thread = nil
19
+
20
+ # Flushes whatever's already tallied on a normal process exit, so the last partial window
21
+ # (anything shorter than a full session_flush_interval) isn't silently dropped.
22
+ at_exit { flush }
23
+ end
24
+
25
+ def record_session(crashed:)
26
+ ensure_worker_started
27
+
28
+ @mutex.synchronize do
29
+ @sessions_count += 1
30
+ @crashed_sessions_count += 1 if crashed
31
+ end
32
+ end
33
+
34
+ # Snapshots and resets the in-process counters, then delivers them. A failed delivery keeps
35
+ # the counts where they are rather than resetting, so the next flush's window just grows
36
+ # instead of losing what was already tallied; there's no other copy of this data anywhere.
37
+ def flush
38
+ snapshot = nil
39
+
40
+ @mutex.synchronize do
41
+ return if @sessions_count.zero?
42
+
43
+ snapshot = {
44
+ release: configuration.release,
45
+ environment: configuration.environment.to_s,
46
+ period_started_at: @period_started_at.iso8601,
47
+ period_ended_at: Time.now.utc.iso8601,
48
+ sessions_count: @sessions_count,
49
+ crashed_sessions_count: @crashed_sessions_count
50
+ }
51
+ end
52
+
53
+ return unless snapshot && client.deliver_session_checkin(snapshot)
54
+
55
+ @mutex.synchronize do
56
+ @sessions_count = 0
57
+ @crashed_sessions_count = 0
58
+ @period_started_at = Time.now.utc
59
+ end
60
+ end
61
+
62
+ private
63
+ attr_reader :configuration, :client
64
+
65
+ def ensure_worker_started
66
+ return if @thread&.alive?
67
+
68
+ @mutex.synchronize do
69
+ return if @thread&.alive?
70
+
71
+ @thread = Thread.new { run }
72
+ @thread.abort_on_exception = false
73
+ end
74
+ end
75
+
76
+ def run
77
+ loop do
78
+ sleep configuration.session_flush_interval
79
+ flush
80
+ rescue StandardError => e
81
+ configuration.logger&.debug { "[ForgeOpsTracker] session flush thread error: #{e.class}: #{e.message}" }
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,3 +1,3 @@
1
1
  module ForgeOpsTracker
2
- VERSION = "0.2.1"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -5,6 +5,8 @@ require "forge_ops_tracker/event_builder"
5
5
  require "forge_ops_tracker/client"
6
6
  require "forge_ops_tracker/delivery_queue"
7
7
  require "forge_ops_tracker/error_subscriber"
8
+ require "forge_ops_tracker/session_flusher"
9
+ require "forge_ops_tracker/middleware/session_tracking"
8
10
 
9
11
  module ForgeOpsTracker
10
12
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forge_ops_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ForgeOps
@@ -23,8 +23,8 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '3.13'
26
- description: Hooks Rails' error reporter and reports exceptions to a private ForgeOps
27
- exception tracker instance over HTTP, without ever raising back into the host application.
26
+ description: Hooks Rails' error reporter and reports exceptions to a ForgeOps exception
27
+ tracker instance over HTTP, without ever raising back into the host application.
28
28
  executables: []
29
29
  extensions: []
30
30
  extra_rdoc_files: []
@@ -38,8 +38,10 @@ files:
38
38
  - lib/forge_ops_tracker/delivery_queue.rb
39
39
  - lib/forge_ops_tracker/error_subscriber.rb
40
40
  - lib/forge_ops_tracker/event_builder.rb
41
+ - lib/forge_ops_tracker/middleware/session_tracking.rb
41
42
  - lib/forge_ops_tracker/pii_scrubber.rb
42
43
  - lib/forge_ops_tracker/railtie.rb
44
+ - lib/forge_ops_tracker/session_flusher.rb
43
45
  - lib/forge_ops_tracker/version.rb
44
46
  homepage: https://getforgeops.net
45
47
  licenses:
@@ -63,5 +65,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
65
  requirements: []
64
66
  rubygems_version: 4.0.11
65
67
  specification_version: 4
66
- summary: Rails exception reporting client for a self-hosted ForgeOps tracker
68
+ summary: Rails exception reporting client for a ForgeOps tracker
67
69
  test_files: []