watchforge-ruby-sdk 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d1d05cc1cbefe1dcac7416fe8487041e18009bc9df6ae09d4c10a8cdbec425b
4
+ data.tar.gz: dcd0fcee33796a40c4e5a5d55cf8490bbd002f10d47b70d3caaef8584b833280
5
+ SHA512:
6
+ metadata.gz: 76caafd0ca6e1d1b38fc69aac06cf838c7bc0f27d28b4fa3e1a227047acf1f44c4e79e3965be2c259a0fde73052f8db755b8e5027ccb2e0ffd35d81050d9fd29
7
+ data.tar.gz: 2ec8d79857a2c587c3e356d2fd0eb0e370ff93a6f943a99a9722fc810436f5183648b6f9cb1dec609e117ff60c046fbb3a4cf15bd4a02315cef249df349ec52f
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WatchForge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # WatchForge Ruby SDK (`watchforge-ruby-sdk`)
2
+
3
+ Ruby SDK for WatchForge — errors, cron monitors, distributed tracing, and management APIs. Aligned with `watchforge-sdk` (Python) and `@watchforge/browser` (JavaScript).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ gem install watchforge-ruby-sdk
9
+ ```
10
+
11
+ Or in your `Gemfile`:
12
+
13
+ ```ruby
14
+ gem "watchforge-ruby-sdk", "~> 0.1.0"
15
+ ```
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```ruby
24
+ require "watchforge"
25
+
26
+ Watchforge.init(
27
+ dsn: "https://PUBLIC_KEY@dev.watchforges.com/PROJECT_ID",
28
+ environment: "production"
29
+ )
30
+
31
+ Watchforge.context.add_breadcrumb("boot", "service started")
32
+ Watchforge.capture_message("hello", level: "info")
33
+ Watchforge.capture_exception(StandardError.new("boom"))
34
+ ```
35
+
36
+ ## Feature support
37
+
38
+ | Feature | API |
39
+ | --- | --- |
40
+ | Issues (ingestion) | `capture_exception`, `capture_message` |
41
+ | Context | `context.add_breadcrumb`, `set_user`, `set_tags` |
42
+ | Cron monitors | `capture_check_in` |
43
+ | Tracing | `start_transaction`, `start_span`, `finish_transaction` |
44
+ | Management API | `Watchforge::API::Client` |
45
+
46
+ ## Cron monitor
47
+
48
+ ```ruby
49
+ check_in_id = Watchforge.capture_check_in(
50
+ monitor_slug: "nightly-job",
51
+ status: "in_progress",
52
+ monitor_config: {
53
+ schedule: { type: "crontab", value: "0 * * * *" },
54
+ timezone: "UTC",
55
+ check_margin: 5,
56
+ max_runtime: 30
57
+ }
58
+ )
59
+
60
+ Watchforge.capture_check_in(
61
+ monitor_slug: "nightly-job",
62
+ status: "ok",
63
+ check_in_id: check_in_id,
64
+ monitor_config: monitor_config
65
+ )
66
+ ```
67
+
68
+ ## Tracing
69
+
70
+ ```ruby
71
+ txn = Watchforge.start_transaction("GET /health", "Health Check")
72
+ span = txn.start_span("db.query", "SELECT 1")
73
+ span.finish
74
+ Watchforge.finish_transaction
75
+ ```
76
+
77
+ ## Management API (Bearer token)
78
+
79
+ ```ruby
80
+ api = Watchforge::API::Client.new("https://dev.watchforges.com", ENV["WATCHFORGE_TOKEN"])
81
+ api.list_issues(project_id, range: "7d", status: "unresolved")
82
+ api.dashboard_summary(project_id: project_id, range: "24h")
83
+ api.query_dashboard_widget(widget_payload)
84
+ ```
85
+
86
+ ## Package naming (WatchForge SDK family)
87
+
88
+ | Language | Package | Install |
89
+ | --- | --- | --- |
90
+ | Python | `watchforge-sdk` | `pip install watchforge-sdk` |
91
+ | JavaScript | `@watchforge/browser` | `npm install @watchforge/browser` |
92
+ | Go | `watchforge-go-sdk` | `go get github.com/watchforges/watchforge-go-sdk/watchforge` |
93
+ | **Ruby** | `watchforge-ruby-sdk` | `gem install watchforge-ruby-sdk` |
94
+ | PHP | `watchforge-php-sdk` | `composer require watchforges/watchforge-php-sdk` |
95
+ | Flutter | `watchforge_flutter_sdk` | `flutter pub add watchforge_flutter_sdk` |
96
+ | React Native | `@watchforge/react-native` | `npm install @watchforge/react-native` |
97
+
98
+ ## Publishing (maintainers)
99
+
100
+ See [PUBLISHING.md](./PUBLISHING.md).
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Watchforge
8
+ module API
9
+ class Client
10
+ def initialize(base_url, token)
11
+ @base_url = base_url.to_s.chomp("/")
12
+ @base_url += "/api" unless @base_url.end_with?("/api")
13
+ @token = token
14
+ end
15
+
16
+ def get(path, params = {})
17
+ request(:get, path, params: params)
18
+ end
19
+
20
+ def post(path, body = {})
21
+ request(:post, path, body: body)
22
+ end
23
+
24
+ def patch(path, body = {})
25
+ request(:patch, path, body: body)
26
+ end
27
+
28
+ def delete(path)
29
+ request(:delete, path)
30
+ end
31
+
32
+ private
33
+
34
+ def request(method, path, params: nil, body: nil)
35
+ uri = URI.parse("#{@base_url}/#{path.delete_prefix('/')}")
36
+ uri.query = URI.encode_www_form(params) if params && !params.empty?
37
+
38
+ klass = {
39
+ get: Net::HTTP::Get,
40
+ post: Net::HTTP::Post,
41
+ patch: Net::HTTP::Patch,
42
+ delete: Net::HTTP::Delete
43
+ }[method]
44
+ request = klass.new(uri)
45
+ request["Authorization"] = "Bearer #{@token}"
46
+ request["Content-Type"] = "application/json"
47
+ request.body = JSON.generate(body) if body
48
+
49
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
50
+ http.request(request)
51
+ end
52
+ raise "watchforge api failed: #{response.code} #{response.body}" if response.code.to_i >= 400
53
+ return nil if response.body.nil? || response.body.empty?
54
+
55
+ JSON.parse(response.body)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Watchforge
4
+ module API
5
+ module Dashboard
6
+ def dashboard_summary(params = {})
7
+ get("dashboard/", params)
8
+ end
9
+
10
+ def dashboard_analytics(params = {})
11
+ get("dashboard/analytics/", params)
12
+ end
13
+
14
+ def dashboard_reports(params = {})
15
+ get("dashboard/reports/", params)
16
+ end
17
+
18
+ def query_dashboard_widget(payload)
19
+ post("dashboard/custom/widgets/query/", payload)
20
+ end
21
+
22
+ def list_custom_dashboards(params = {})
23
+ get("dashboard/custom/", params)
24
+ end
25
+
26
+ def project_dashboard(project_id, params = {})
27
+ get("projects/#{project_id}/dashboard/", params)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Watchforge::API::Client.include Watchforge::API::Dashboard
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Watchforge
4
+ module API
5
+ module Issues
6
+ def list_issues(project_id, params = {})
7
+ get("issues/", params.merge(project_id: project_id))
8
+ end
9
+
10
+ def get_issue(issue_id)
11
+ get("issues/#{issue_id}/")
12
+ end
13
+
14
+ def resolve_issue(issue_id)
15
+ post("issues/#{issue_id}/resolve/")
16
+ end
17
+
18
+ def ignore_issue(issue_id)
19
+ post("issues/#{issue_id}/ignore/")
20
+ end
21
+
22
+ def update_issue(issue_id, payload)
23
+ patch("issues/#{issue_id}/update/", payload)
24
+ end
25
+
26
+ def issue_stats(issue_id, params = {})
27
+ get("issues/#{issue_id}/stats/", params)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Watchforge::API::Client.include Watchforge::API::Issues
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Watchforge
4
+ module API
5
+ module Monitors
6
+ def list_monitors(project_id)
7
+ get("monitors/projects/#{project_id}/monitors/")
8
+ end
9
+
10
+ def get_monitor(monitor_id)
11
+ get("monitors/monitors/#{monitor_id}/")
12
+ end
13
+
14
+ def create_monitor(project_id, payload)
15
+ post("monitors/projects/#{project_id}/monitors/", payload)
16
+ end
17
+
18
+ def update_monitor(monitor_id, payload)
19
+ patch("monitors/monitors/#{monitor_id}/", payload)
20
+ end
21
+
22
+ def delete_monitor(monitor_id)
23
+ delete("monitors/monitors/#{monitor_id}/")
24
+ end
25
+
26
+ def list_monitor_check_ins(monitor_id, params = {})
27
+ get("monitors/monitors/#{monitor_id}/checkins/", params)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Watchforge::API::Client.include Watchforge::API::Monitors
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Watchforge
4
+ module API
5
+ module Traces
6
+ def list_traces(project_id, params = {})
7
+ get("traces/", params.merge(project_id: project_id))
8
+ end
9
+
10
+ def get_trace(trace_id)
11
+ get("traces/#{trace_id}/")
12
+ end
13
+
14
+ def list_trace_spans(trace_id)
15
+ get("traces/#{trace_id}/spans/")
16
+ end
17
+
18
+ def trace_performance(project_id, params = {})
19
+ get("traces/performance/", params.merge(project_id: project_id))
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ Watchforge::API::Client.include Watchforge::API::Traces
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module Watchforge
7
+ class Client
8
+ attr_reader :context
9
+
10
+ def initialize(dsn:, environment: "production", release: nil, debug: false)
11
+ @dsn = dsn
12
+ @environment = environment
13
+ @release = release
14
+ @debug = debug
15
+ @transport = Transport.new(dsn, debug: debug)
16
+ @context = Context.new
17
+ end
18
+
19
+ def capture_exception(error, extra: nil)
20
+ payload = base_event("error", error.message)
21
+ payload[:exception_type] = error.class.name
22
+ payload[:exception_value] = error.message
23
+ payload[:stacktrace] = { frames: [{ filename: "unknown", function: error.class.name, lineno: 0, in_app: true }] }
24
+ merge_extra(payload, extra)
25
+ @transport.send_event(payload)
26
+ end
27
+
28
+ def capture_message(message, level: "info", extra: nil)
29
+ payload = base_event(level, message)
30
+ merge_extra(payload, extra)
31
+ @transport.send_event(payload)
32
+ end
33
+
34
+ def capture_check_in(monitor_slug:, status: "ok", monitor_config: nil, check_in_id: nil, environment: nil)
35
+ check_in_id ||= SecureRandom.uuid if status == "in_progress"
36
+ payload = {
37
+ monitor_slug: monitor_slug,
38
+ status: status,
39
+ environment: environment || @environment
40
+ }
41
+ payload[:monitor_config] = monitor_config if monitor_config
42
+ payload[:check_in_id] = check_in_id if check_in_id
43
+ @transport.send_monitor_check_in(payload)
44
+ check_in_id
45
+ end
46
+
47
+ private
48
+
49
+ def base_event(level, message)
50
+ user, tags, extra, request, breadcrumbs = @context.snapshot
51
+ payload = {
52
+ event_id: SecureRandom.uuid,
53
+ level: level,
54
+ environment: @environment,
55
+ message: message,
56
+ platform: "ruby",
57
+ timestamp: Time.now.utc.iso8601,
58
+ sdk: { name: SDK_NAME, version: VERSION },
59
+ data: {},
60
+ contexts: { runtime: { name: "ruby", version: RUBY_VERSION } }
61
+ }
62
+ payload[:release] = @release if @release
63
+ payload[:user] = user if user
64
+ payload[:request] = request if request
65
+ payload[:tags] = tags unless tags.empty?
66
+ payload[:data].merge!(extra) unless extra.empty?
67
+ payload[:breadcrumbs] = breadcrumbs unless breadcrumbs.empty?
68
+ payload
69
+ end
70
+
71
+ def merge_extra(payload, extra)
72
+ return unless extra
73
+
74
+ payload[:data].merge!(extra)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module Watchforge
7
+ class Context
8
+ def initialize
9
+ @mutex = Mutex.new
10
+ @user = nil
11
+ @tags = {}
12
+ @extra = {}
13
+ @request = nil
14
+ @breadcrumbs = []
15
+ end
16
+
17
+ def set_user(user) = synchronize { @user = user }
18
+ def set_tags(tags) = synchronize { @tags.merge!(tags) }
19
+ def set_extra(extra) = synchronize { @extra.merge!(extra) }
20
+ def set_request(request) = synchronize { @request = request }
21
+
22
+ def add_breadcrumb(category, message, data = nil)
23
+ synchronize do
24
+ @breadcrumbs << {
25
+ category: category,
26
+ message: message,
27
+ level: "info",
28
+ timestamp: Time.now.utc.iso8601,
29
+ data: data
30
+ }
31
+ @breadcrumbs = @breadcrumbs.last(100)
32
+ end
33
+ end
34
+
35
+ def snapshot
36
+ synchronize do
37
+ [@user, @tags.dup, @extra.dup, @request, @breadcrumbs.dup]
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def synchronize(&block)
44
+ @mutex.synchronize(&block)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Watchforge
6
+ ParsedDSN = Struct.new(:scheme, :public_key, :host, :port, :project_id, :dsn, keyword_init: true) do
7
+ def events_url
8
+ ingestion_url("events/")
9
+ end
10
+
11
+ def trace_url
12
+ ingestion_url("trace/")
13
+ end
14
+
15
+ def monitor_url
16
+ ingestion_url("monitors/checkins/")
17
+ end
18
+
19
+ private
20
+
21
+ def ingestion_url(path)
22
+ host = port && !port.empty? ? "#{self.host}:#{port}" : self.host
23
+ "#{scheme}://#{host}/api/ingestion/#{path}"
24
+ end
25
+ end
26
+
27
+ module DSN
28
+ module_function
29
+
30
+ def parse(dsn)
31
+ uri = URI.parse(dsn)
32
+ raise ArgumentError, "invalid watchforge dsn" if uri.user.nil? || uri.host.nil? || uri.path.nil? || uri.path == "/"
33
+
34
+ scheme = %w[localhost 127.0.0.1].include?(uri.host) ? "http" : uri.scheme
35
+ ParsedDSN.new(
36
+ scheme: scheme,
37
+ public_key: uri.user,
38
+ host: uri.host,
39
+ port: uri.port,
40
+ project_id: uri.path.delete_prefix("/"),
41
+ dsn: dsn
42
+ )
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module Watchforge
7
+ class Span
8
+ attr_reader :span_id, :op, :description, :parent_span_id, :data, :tags
9
+ attr_accessor :status, :status_code, :finish_timestamp, :duration_ms
10
+
11
+ def initialize(op, description = "", parent_span_id: nil, data: nil)
12
+ @span_id = SecureRandom.uuid
13
+ @op = op
14
+ @description = description
15
+ @parent_span_id = parent_span_id
16
+ @data = data || {}
17
+ @tags = {}
18
+ @start_timestamp = (Time.now.to_f * 1000).to_i
19
+ @status = "ok"
20
+ end
21
+
22
+ def finish(status = "ok", status_code: nil)
23
+ @status = status
24
+ @status_code = status_code
25
+ @finish_timestamp = (Time.now.to_f * 1000).to_i
26
+ @duration_ms = @finish_timestamp - @start_timestamp
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ span_id: span_id,
32
+ parent_span_id: parent_span_id,
33
+ op: op,
34
+ description: description,
35
+ start_timestamp: Time.at(@start_timestamp / 1000.0).utc.iso8601,
36
+ finish_timestamp: finish_timestamp ? Time.at(finish_timestamp / 1000.0).utc.iso8601 : nil,
37
+ duration_ms: duration_ms,
38
+ status: status,
39
+ status_code: status_code,
40
+ data: data,
41
+ tags: tags,
42
+ timestamp: Time.at(@start_timestamp / 1000.0).utc.iso8601
43
+ }
44
+ end
45
+ end
46
+
47
+ class Transaction
48
+ attr_reader :trace_id, :transaction, :transaction_name, :op, :spans, :tags
49
+
50
+ def initialize(client, transaction, transaction_name, op)
51
+ @client = client
52
+ @trace_id = SecureRandom.uuid
53
+ @transaction = transaction
54
+ @transaction_name = transaction_name || transaction
55
+ @op = op || "http.server"
56
+ @spans = []
57
+ @tags = {}
58
+ @start_timestamp = (Time.now.to_f * 1000).to_i
59
+ @status = "ok"
60
+ end
61
+
62
+ def start_span(op, description = "", data: nil)
63
+ parent = spans.last&.span_id
64
+ span = Span.new(op, description, parent_span_id: parent, data: data)
65
+ spans << span
66
+ span
67
+ end
68
+
69
+ def finish(status = "ok")
70
+ @status = status
71
+ finish_timestamp = (Time.now.to_f * 1000).to_i
72
+ spans.each { |span| span.finish if span.finish_timestamp.nil? }
73
+ payload = {
74
+ trace_id: trace_id,
75
+ transaction: transaction,
76
+ transaction_name: transaction_name,
77
+ op: op,
78
+ start_timestamp: Time.at(@start_timestamp / 1000.0).utc.iso8601,
79
+ finish_timestamp: Time.at(finish_timestamp / 1000.0).utc.iso8601,
80
+ duration_ms: finish_timestamp - @start_timestamp,
81
+ status: @status,
82
+ environment: @client.instance_variable_get(:@environment),
83
+ platform: "ruby",
84
+ sdk_name: SDK_NAME,
85
+ sdk_version: VERSION,
86
+ tags: tags,
87
+ spans: spans.map(&:to_h)
88
+ }
89
+ @client.instance_variable_get(:@transport).send_trace(payload)
90
+ end
91
+ end
92
+
93
+ module Tracing
94
+ @stack = []
95
+
96
+ class << self
97
+ def start_transaction(transaction, transaction_name = nil, op = "http.server")
98
+ client = Watchforge.client
99
+ return nil unless client
100
+
101
+ txn = Transaction.new(client, transaction, transaction_name, op)
102
+ @stack << txn
103
+ txn
104
+ end
105
+
106
+ def current_transaction
107
+ @stack.last
108
+ end
109
+
110
+ def finish_transaction(status = "ok")
111
+ txn = @stack.pop
112
+ txn&.finish(status)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Watchforge
8
+ class Transport
9
+ def initialize(dsn, debug: false)
10
+ @dsn = dsn
11
+ @parsed = DSN.parse(dsn)
12
+ @debug = debug
13
+ end
14
+
15
+ def send_event(payload)
16
+ post(@parsed.events_url, payload)
17
+ end
18
+
19
+ def send_trace(payload)
20
+ post(@parsed.trace_url, payload)
21
+ end
22
+
23
+ def send_monitor_check_in(payload)
24
+ post(@parsed.monitor_url, payload)
25
+ end
26
+
27
+ private
28
+
29
+ def post(url, payload)
30
+ override = ENV["WATCHFORGE_API_URL"]
31
+ url = override if override && url == @parsed.events_url
32
+
33
+ uri = URI.parse(url)
34
+ request = Net::HTTP::Post.new(uri)
35
+ request["Authorization"] = "DSN #{@dsn}"
36
+ request["Content-Type"] = "application/json"
37
+ request.body = JSON.generate(payload)
38
+
39
+ puts "[#{SDK_NAME}] POST #{url}" if @debug
40
+
41
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
42
+ response = http.request(request)
43
+ raise "watchforge ingestion failed: #{response.code}" if response.code.to_i >= 400
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Watchforge
4
+ VERSION = "0.1.1"
5
+ SDK_NAME = "watchforge-ruby-sdk"
6
+ end
data/lib/watchforge.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "watchforge/version"
4
+ require "watchforge/dsn"
5
+ require "watchforge/transport"
6
+ require "watchforge/context"
7
+ require "watchforge/client"
8
+ require "watchforge/tracing"
9
+ require "watchforge/api/client"
10
+ require "watchforge/api/issues"
11
+ require "watchforge/api/monitors"
12
+ require "watchforge/api/traces"
13
+ require "watchforge/api/dashboard"
14
+
15
+ module Watchforge
16
+ class << self
17
+ def init(dsn:, environment: "production", release: nil, debug: false)
18
+ @client = Client.new(dsn: dsn, environment: environment, release: release, debug: debug)
19
+ end
20
+
21
+ def client
22
+ @client
23
+ end
24
+
25
+ def capture_exception(error, extra: nil)
26
+ client&.capture_exception(error, extra: extra)
27
+ end
28
+
29
+ def capture_message(message, level: "info", extra: nil)
30
+ client&.capture_message(message, level: level, extra: extra)
31
+ end
32
+
33
+ def capture_check_in(monitor_slug:, status: "ok", monitor_config: nil, check_in_id: nil, environment: nil)
34
+ client&.capture_check_in(
35
+ monitor_slug: monitor_slug,
36
+ status: status,
37
+ monitor_config: monitor_config,
38
+ check_in_id: check_in_id,
39
+ environment: environment
40
+ )
41
+ end
42
+
43
+ def start_transaction(transaction, transaction_name = nil, op = "http.server")
44
+ Tracing.start_transaction(transaction, transaction_name, op)
45
+ end
46
+
47
+ def get_current_transaction
48
+ Tracing.current_transaction
49
+ end
50
+
51
+ def finish_transaction(status = "ok")
52
+ Tracing.finish_transaction(status)
53
+ end
54
+
55
+ def context
56
+ client&.context || Context.new
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/watchforge/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "watchforge-ruby-sdk"
7
+ spec.version = ::Watchforge::VERSION
8
+ spec.authors = ["WatchForge"]
9
+ spec.email = ["support@watchforges.com"]
10
+ spec.summary = "WatchForge Ruby SDK for errors, traces, cron monitors, and management APIs"
11
+ spec.description = spec.summary
12
+ spec.homepage = "https://watchforges.com"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.7.0"
15
+
16
+ spec.metadata = {
17
+ "source_code_uri" => "https://github.com/watchforges/watchforge-ruby-sdk",
18
+ "rubygems_mfa_required" => "true"
19
+ }
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.start_with?("examples/") }
23
+ rescue StandardError
24
+ Dir["lib/**/*.rb", "README.md", "LICENSE", "PUBLISHING.md"]
25
+ end
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "json", "~> 2.6"
29
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: watchforge-ruby-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - WatchForge
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ description: WatchForge Ruby SDK for errors, traces, cron monitors, and management
28
+ APIs
29
+ email:
30
+ - support@watchforges.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE
36
+ - README.md
37
+ - lib/watchforge.rb
38
+ - lib/watchforge/api/client.rb
39
+ - lib/watchforge/api/dashboard.rb
40
+ - lib/watchforge/api/issues.rb
41
+ - lib/watchforge/api/monitors.rb
42
+ - lib/watchforge/api/traces.rb
43
+ - lib/watchforge/client.rb
44
+ - lib/watchforge/context.rb
45
+ - lib/watchforge/dsn.rb
46
+ - lib/watchforge/tracing.rb
47
+ - lib/watchforge/transport.rb
48
+ - lib/watchforge/version.rb
49
+ - watchforge-ruby-sdk.gemspec
50
+ homepage: https://watchforges.com
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ source_code_uri: https://github.com/watchforges/watchforge-ruby-sdk
55
+ rubygems_mfa_required: 'true'
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 2.7.0
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.0.3.1
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: WatchForge Ruby SDK for errors, traces, cron monitors, and management APIs
75
+ test_files: []