justanalytics 0.1.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 +7 -0
- data/README.md +103 -0
- data/lib/justanalytics/client.rb +374 -0
- data/lib/justanalytics/configuration.rb +71 -0
- data/lib/justanalytics/context.rb +122 -0
- data/lib/justanalytics/integrations/net_http.rb +125 -0
- data/lib/justanalytics/integrations/rails.rb +124 -0
- data/lib/justanalytics/integrations/sidekiq.rb +98 -0
- data/lib/justanalytics/logger.rb +150 -0
- data/lib/justanalytics/span.rb +170 -0
- data/lib/justanalytics/trace_context.rb +85 -0
- data/lib/justanalytics/transport.rb +204 -0
- data/lib/justanalytics/version.rb +6 -0
- data/lib/justanalytics.rb +208 -0
- metadata +117 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7fb7497381f0737b13c5ac642233e584d8a6761305a761deb076c3097b182e60
|
|
4
|
+
data.tar.gz: 1ba4d29f18dd7add59d685c6e37cc8f94a23a9b4a9bea91e8777040d90e296f7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 69f41c7257fd38b4b77d1a5920cabc2f23203da91934130f9c902f1713a5b9138302c44870da936b7318d69f45d2b79f86dbb5c1c52d306b23bedb2eba2310aa
|
|
7
|
+
data.tar.gz: a177161441f4ba169a54cb5af6c2b7b8ba4b5a60813d3432c7a6f71587db0381fcdbbc42f1fa03876d82cec3deb6ae90bbc72c9277146a52f9e088c51e57f5e2
|
data/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# JustAnalytics Ruby SDK
|
|
2
|
+
|
|
3
|
+
End-to-end observability for Ruby applications. Distributed tracing, error tracking, structured logging, and infrastructure metrics — all in one gem.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "justanalytics"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run `bundle install`.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "justanalytics"
|
|
19
|
+
|
|
20
|
+
JustAnalytics.init(
|
|
21
|
+
site_id: ENV["JA_SITE_ID"],
|
|
22
|
+
api_key: ENV["JA_API_KEY"],
|
|
23
|
+
service_name: "rails-api",
|
|
24
|
+
environment: "production",
|
|
25
|
+
release: "1.2.3"
|
|
26
|
+
)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Tracing
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
JustAnalytics.start_span("process-order", op: "task") do |span|
|
|
35
|
+
span.set_attribute("order.id", order.id)
|
|
36
|
+
span.add_event("payment.started")
|
|
37
|
+
process_payment(order)
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Error Tracking
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
begin
|
|
45
|
+
risky_operation
|
|
46
|
+
rescue => e
|
|
47
|
+
JustAnalytics.capture_exception(e, tags: { module: "payments" })
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
JustAnalytics.capture_message("Rate limit exceeded", level: "warning")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Structured Logging
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
JustAnalytics.logger.info("User logged in", user_id: "u123")
|
|
57
|
+
JustAnalytics.logger.error("Payment failed", order_id: "o456")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### User Context
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
JustAnalytics.set_user(id: "user-123", email: "alice@example.com")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Rails Integration
|
|
67
|
+
|
|
68
|
+
The SDK auto-configures Rails middleware via a Railtie. Just add the gem and initialize:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# config/initializers/justanalytics.rb
|
|
72
|
+
JustAnalytics.init(
|
|
73
|
+
site_id: ENV["JA_SITE_ID"],
|
|
74
|
+
api_key: ENV["JA_API_KEY"],
|
|
75
|
+
service_name: "rails-api"
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Sidekiq Integration
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Sidekiq.configure_server do |config|
|
|
83
|
+
config.server_middleware do |chain|
|
|
84
|
+
chain.add JustAnalytics::SidekiqServerMiddleware
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Sidekiq.configure_client do |config|
|
|
89
|
+
config.client_middleware do |chain|
|
|
90
|
+
chain.add JustAnalytics::SidekiqClientMiddleware
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Net::HTTP Auto-Instrumentation
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
JustAnalytics::NetHttpPatch.enable!
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module JustAnalytics
|
|
7
|
+
# Core client class managing configuration, lifecycle, span creation,
|
|
8
|
+
# error capture, context propagation, and batched transport.
|
|
9
|
+
#
|
|
10
|
+
# Typically accessed through the module-level methods on {JustAnalytics}
|
|
11
|
+
# rather than instantiated directly.
|
|
12
|
+
class Client
|
|
13
|
+
# @return [Configuration] current configuration
|
|
14
|
+
attr_reader :config
|
|
15
|
+
|
|
16
|
+
# @return [Logger] structured logger instance
|
|
17
|
+
attr_reader :logger
|
|
18
|
+
|
|
19
|
+
# @return [Boolean] whether init has been called
|
|
20
|
+
attr_reader :initialized
|
|
21
|
+
alias_method :initialized?, :initialized
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@config = nil
|
|
25
|
+
@transport = nil
|
|
26
|
+
@logger = nil
|
|
27
|
+
@initialized = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Initialize the SDK with configuration.
|
|
31
|
+
#
|
|
32
|
+
# @param site_id [String] site ID (required)
|
|
33
|
+
# @param api_key [String] API key (required)
|
|
34
|
+
# @param service_name [String] service name (required)
|
|
35
|
+
# @param environment [String, nil] deployment environment
|
|
36
|
+
# @param release [String, nil] release/version string
|
|
37
|
+
# @param server_url [String, nil] JA server URL override
|
|
38
|
+
# @param debug [Boolean] enable debug logging
|
|
39
|
+
# @param flush_interval [Float] flush interval in seconds
|
|
40
|
+
# @param max_batch_size [Integer] max batch size
|
|
41
|
+
# @param enabled [Boolean] enable/disable SDK
|
|
42
|
+
# @return [void]
|
|
43
|
+
def init(site_id:, api_key:, service_name:, environment: nil, release: nil,
|
|
44
|
+
server_url: nil, debug: false, flush_interval: 2.0, max_batch_size: 100,
|
|
45
|
+
enabled: true)
|
|
46
|
+
if @initialized
|
|
47
|
+
$stderr.puts "[JustAnalytics] Warning: init() already called. Ignoring."
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@config = Configuration.new
|
|
52
|
+
@config.site_id = site_id
|
|
53
|
+
@config.api_key = api_key
|
|
54
|
+
@config.service_name = service_name
|
|
55
|
+
@config.environment = environment
|
|
56
|
+
@config.release = release
|
|
57
|
+
@config.server_url = server_url if server_url
|
|
58
|
+
@config.debug = debug
|
|
59
|
+
@config.flush_interval = flush_interval
|
|
60
|
+
@config.max_batch_size = max_batch_size
|
|
61
|
+
@config.enabled = enabled
|
|
62
|
+
|
|
63
|
+
@config.validate!
|
|
64
|
+
|
|
65
|
+
if debug
|
|
66
|
+
key_prefix = api_key[0, 10]
|
|
67
|
+
$stderr.puts "[JustAnalytics] Initializing SDK: service=#{service_name}, " \
|
|
68
|
+
"server=#{@config.server_url}, apiKey=#{key_prefix}..., enabled=#{enabled}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if enabled
|
|
72
|
+
@transport = Transport.new(
|
|
73
|
+
server_url: @config.server_url,
|
|
74
|
+
api_key: @config.api_key,
|
|
75
|
+
site_id: @config.site_id,
|
|
76
|
+
flush_interval: @config.flush_interval,
|
|
77
|
+
max_batch_size: @config.max_batch_size,
|
|
78
|
+
debug: @config.debug
|
|
79
|
+
)
|
|
80
|
+
@transport.start
|
|
81
|
+
|
|
82
|
+
@logger = JustAnalytics::Logger.new(
|
|
83
|
+
service_name: @config.service_name,
|
|
84
|
+
transport: @transport,
|
|
85
|
+
enabled: true,
|
|
86
|
+
environment: @config.environment,
|
|
87
|
+
release: @config.release,
|
|
88
|
+
debug: @config.debug
|
|
89
|
+
)
|
|
90
|
+
else
|
|
91
|
+
@logger = JustAnalytics::Logger.new(
|
|
92
|
+
service_name: service_name,
|
|
93
|
+
transport: nil,
|
|
94
|
+
enabled: false
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@initialized = true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Create and execute a span.
|
|
102
|
+
#
|
|
103
|
+
# The block runs within a context scope with the span as the active span.
|
|
104
|
+
# Nested +start_span+ calls automatically create parent-child relationships.
|
|
105
|
+
#
|
|
106
|
+
# @param name [String] operation name for the span
|
|
107
|
+
# @param op [String] span kind (default: "internal")
|
|
108
|
+
# @param attributes [Hash] initial span attributes
|
|
109
|
+
# @yield [Span] the created span
|
|
110
|
+
# @return the return value of the block
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# result = JustAnalytics.start_span("process-order", op: "task") do |span|
|
|
114
|
+
# span.set_attribute("order.id", "12345")
|
|
115
|
+
# process_order
|
|
116
|
+
# end
|
|
117
|
+
def start_span(name, op: "internal", attributes: {})
|
|
118
|
+
# Determine parent and trace ID
|
|
119
|
+
parent_span = Context.current_span
|
|
120
|
+
if parent_span
|
|
121
|
+
trace_id = parent_span.trace_id
|
|
122
|
+
parent_span_id = parent_span.id
|
|
123
|
+
else
|
|
124
|
+
trace_id = TraceContext.generate_trace_id
|
|
125
|
+
parent_span_id = nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
should_enqueue = @initialized && @config&.enabled
|
|
129
|
+
|
|
130
|
+
# Merge context tags into attributes
|
|
131
|
+
str_attrs = {}
|
|
132
|
+
attributes.each { |k, v| str_attrs[k.to_s] = v }
|
|
133
|
+
merged_attrs = Context.current_tags.merge(str_attrs)
|
|
134
|
+
merged_attrs["environment"] = @config.environment if @config&.environment
|
|
135
|
+
merged_attrs["release"] = @config.release if @config&.release
|
|
136
|
+
|
|
137
|
+
span = Span.new(
|
|
138
|
+
operation_name: name,
|
|
139
|
+
service_name: @config&.service_name || "unknown",
|
|
140
|
+
kind: op,
|
|
141
|
+
trace_id: trace_id,
|
|
142
|
+
parent_span_id: parent_span_id,
|
|
143
|
+
attributes: merged_attrs
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
child_ctx = Context.create_child(active_span: span, trace_id: trace_id)
|
|
147
|
+
|
|
148
|
+
result = nil
|
|
149
|
+
Context.with_context(child_ctx) do
|
|
150
|
+
result = yield(span) if block_given?
|
|
151
|
+
rescue => e
|
|
152
|
+
span.set_status("error", e.message) unless span.ended?
|
|
153
|
+
span.end_span unless span.ended?
|
|
154
|
+
enqueue_span(span) if should_enqueue
|
|
155
|
+
raise
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
span.end_span unless span.ended?
|
|
159
|
+
enqueue_span(span) if should_enqueue
|
|
160
|
+
result
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Capture an exception and send it to JustAnalytics.
|
|
164
|
+
#
|
|
165
|
+
# @param error [Exception, String, Object] the error to capture
|
|
166
|
+
# @param tags [Hash] additional tags
|
|
167
|
+
# @param extra [Hash] arbitrary extra data
|
|
168
|
+
# @param user [Hash] user context override
|
|
169
|
+
# @param level [String] severity level (default: "error")
|
|
170
|
+
# @param fingerprint [Array<String>] custom fingerprint
|
|
171
|
+
# @return [String] unique event ID, or empty string if disabled
|
|
172
|
+
def capture_exception(error, tags: {}, extra: {}, user: nil, level: "error", fingerprint: nil)
|
|
173
|
+
return "" unless @initialized && @config&.enabled
|
|
174
|
+
|
|
175
|
+
payload = build_error_payload(
|
|
176
|
+
error,
|
|
177
|
+
mechanism_type: "manual",
|
|
178
|
+
handled: true,
|
|
179
|
+
level: level,
|
|
180
|
+
tags: tags,
|
|
181
|
+
extra: extra,
|
|
182
|
+
user: user,
|
|
183
|
+
fingerprint: fingerprint
|
|
184
|
+
)
|
|
185
|
+
@transport&.enqueue_error(payload)
|
|
186
|
+
payload["eventId"]
|
|
187
|
+
rescue => e
|
|
188
|
+
$stderr.puts "[JustAnalytics] captureException() internal error: #{e.message}" if @config&.debug
|
|
189
|
+
""
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Capture a message and send it to JustAnalytics.
|
|
193
|
+
#
|
|
194
|
+
# @param message [String] the message string
|
|
195
|
+
# @param level [String] severity level (default: "info")
|
|
196
|
+
# @param tags [Hash] additional tags
|
|
197
|
+
# @return [String] unique event ID, or empty string if disabled
|
|
198
|
+
def capture_message(message, level: "info", tags: {})
|
|
199
|
+
return "" unless @initialized && @config&.enabled
|
|
200
|
+
|
|
201
|
+
payload = build_error_payload(
|
|
202
|
+
message,
|
|
203
|
+
mechanism_type: "manual",
|
|
204
|
+
handled: true,
|
|
205
|
+
level: level,
|
|
206
|
+
tags: tags,
|
|
207
|
+
extra: {},
|
|
208
|
+
user: nil,
|
|
209
|
+
fingerprint: nil
|
|
210
|
+
)
|
|
211
|
+
payload["error"]["type"] = "Message"
|
|
212
|
+
@transport&.enqueue_error(payload)
|
|
213
|
+
payload["eventId"]
|
|
214
|
+
rescue => e
|
|
215
|
+
$stderr.puts "[JustAnalytics] captureMessage() internal error: #{e.message}" if @config&.debug
|
|
216
|
+
""
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Set user context for the current thread scope.
|
|
220
|
+
#
|
|
221
|
+
# @param id [String, nil] user ID
|
|
222
|
+
# @param email [String, nil] user email
|
|
223
|
+
# @param username [String, nil] username
|
|
224
|
+
# @return [void]
|
|
225
|
+
def set_user(id: nil, email: nil, username: nil)
|
|
226
|
+
return unless @initialized
|
|
227
|
+
|
|
228
|
+
user = {}
|
|
229
|
+
user["id"] = id if id
|
|
230
|
+
user["email"] = email if email
|
|
231
|
+
user["username"] = username if username
|
|
232
|
+
Context.set_user(user)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Set a tag on the current thread scope.
|
|
236
|
+
#
|
|
237
|
+
# @param key [String] tag key
|
|
238
|
+
# @param value [String] tag value
|
|
239
|
+
# @return [void]
|
|
240
|
+
def set_tag(key, value)
|
|
241
|
+
return unless @initialized
|
|
242
|
+
|
|
243
|
+
Context.set_tag(key, value)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Record a custom infrastructure metric.
|
|
247
|
+
#
|
|
248
|
+
# @param metric_name [String] metric name (dot notation)
|
|
249
|
+
# @param value [Numeric] metric value
|
|
250
|
+
# @param tags [Hash] optional tags
|
|
251
|
+
# @return [void]
|
|
252
|
+
def record_metric(metric_name, value, tags: {})
|
|
253
|
+
return unless @initialized && @config&.enabled
|
|
254
|
+
|
|
255
|
+
payload = {
|
|
256
|
+
"metricName" => metric_name,
|
|
257
|
+
"value" => value,
|
|
258
|
+
"serviceName" => @config.service_name,
|
|
259
|
+
"timestamp" => Time.now.utc.iso8601(3),
|
|
260
|
+
"tags" => {
|
|
261
|
+
"hostname" => Socket.gethostname,
|
|
262
|
+
"runtime" => "ruby",
|
|
263
|
+
"rubyVersion" => RUBY_VERSION
|
|
264
|
+
}.merge(@config.environment ? { "environment" => @config.environment } : {})
|
|
265
|
+
.merge(tags.transform_keys(&:to_s))
|
|
266
|
+
}
|
|
267
|
+
@transport&.enqueue_metric(payload)
|
|
268
|
+
rescue => e
|
|
269
|
+
$stderr.puts "[JustAnalytics] recordMetric() internal error: #{e.message}" if @config&.debug
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Flush all pending data to the server.
|
|
273
|
+
#
|
|
274
|
+
# @return [void]
|
|
275
|
+
def flush
|
|
276
|
+
@transport&.flush
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Shut down the SDK: flush remaining data, stop worker thread.
|
|
280
|
+
#
|
|
281
|
+
# @return [void]
|
|
282
|
+
def close
|
|
283
|
+
return unless @initialized
|
|
284
|
+
|
|
285
|
+
$stderr.puts "[JustAnalytics] Closing SDK..." if @config&.debug
|
|
286
|
+
|
|
287
|
+
@transport&.shutdown
|
|
288
|
+
@transport = nil
|
|
289
|
+
@logger = nil
|
|
290
|
+
@initialized = false
|
|
291
|
+
|
|
292
|
+
$stderr.puts "[JustAnalytics] SDK closed." if @config&.debug
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
# Enqueue a completed span for transport.
|
|
298
|
+
def enqueue_span(span)
|
|
299
|
+
@transport&.enqueue_span(span.to_h)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Build an error payload matching the /api/ingest/errors schema.
|
|
303
|
+
#
|
|
304
|
+
# @param value [Exception, String, Object] the error or message
|
|
305
|
+
# @param mechanism_type [String] how the error was captured
|
|
306
|
+
# @param handled [Boolean] whether the error was handled
|
|
307
|
+
# @param level [String] severity level
|
|
308
|
+
# @param tags [Hash] additional tags
|
|
309
|
+
# @param extra [Hash] extra data
|
|
310
|
+
# @param user [Hash, nil] user context override
|
|
311
|
+
# @param fingerprint [Array<String>, nil] custom fingerprint
|
|
312
|
+
# @return [Hash] the serialized payload
|
|
313
|
+
def build_error_payload(value, mechanism_type:, handled:, level:, tags:, extra:, user:, fingerprint:)
|
|
314
|
+
error_data = serialize_error(value)
|
|
315
|
+
|
|
316
|
+
# Get trace context
|
|
317
|
+
active_span = Context.current_span
|
|
318
|
+
trace_id = active_span&.trace_id || Context.current_trace_id
|
|
319
|
+
span_id = active_span&.id
|
|
320
|
+
|
|
321
|
+
# Get user context (parameter overrides context)
|
|
322
|
+
context_user = Context.current_user
|
|
323
|
+
resolved_user = user || context_user
|
|
324
|
+
|
|
325
|
+
# Merge context tags with provided tags
|
|
326
|
+
context_tags = Context.current_tags
|
|
327
|
+
merged_tags = context_tags.merge((tags || {}).transform_keys(&:to_s).transform_values(&:to_s))
|
|
328
|
+
|
|
329
|
+
event_id = SecureRandom.hex(12)
|
|
330
|
+
|
|
331
|
+
{
|
|
332
|
+
"eventId" => event_id,
|
|
333
|
+
"timestamp" => Time.now.utc.iso8601(3),
|
|
334
|
+
"error" => error_data,
|
|
335
|
+
"level" => level,
|
|
336
|
+
"mechanism" => {
|
|
337
|
+
"type" => mechanism_type,
|
|
338
|
+
"handled" => handled
|
|
339
|
+
},
|
|
340
|
+
"context" => {
|
|
341
|
+
"serviceName" => @config.service_name,
|
|
342
|
+
"environment" => @config.environment,
|
|
343
|
+
"release" => @config.release,
|
|
344
|
+
"runtime" => "ruby",
|
|
345
|
+
"rubyVersion" => RUBY_VERSION
|
|
346
|
+
},
|
|
347
|
+
"trace" => trace_id ? { "traceId" => trace_id, "spanId" => span_id } : nil,
|
|
348
|
+
"user" => resolved_user && !resolved_user.empty? ? resolved_user : nil,
|
|
349
|
+
"tags" => merged_tags.empty? ? nil : merged_tags,
|
|
350
|
+
"extra" => (extra || {}).empty? ? nil : extra.transform_keys(&:to_s),
|
|
351
|
+
"fingerprint" => fingerprint
|
|
352
|
+
}
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Serialize an error value into the error payload shape.
|
|
356
|
+
#
|
|
357
|
+
# @param value [Exception, String, Object] the error to serialize
|
|
358
|
+
# @return [Hash] with "message", "type", "stack" keys
|
|
359
|
+
def serialize_error(value)
|
|
360
|
+
case value
|
|
361
|
+
when Exception
|
|
362
|
+
{
|
|
363
|
+
"message" => value.message.to_s.empty? ? value.class.name : value.message,
|
|
364
|
+
"type" => value.class.name,
|
|
365
|
+
"stack" => value.backtrace&.join("\n")
|
|
366
|
+
}
|
|
367
|
+
when String
|
|
368
|
+
{ "message" => value, "type" => "Error", "stack" => nil }
|
|
369
|
+
else
|
|
370
|
+
{ "message" => value.to_s, "type" => "Error", "stack" => nil }
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JustAnalytics
|
|
4
|
+
# Configuration object holding all SDK settings.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# JustAnalytics.init(
|
|
8
|
+
# site_id: "site_abc123",
|
|
9
|
+
# api_key: "ja_sk_your_api_key_here",
|
|
10
|
+
# service_name: "rails-api",
|
|
11
|
+
# environment: "production",
|
|
12
|
+
# release: "1.2.3"
|
|
13
|
+
# )
|
|
14
|
+
class Configuration
|
|
15
|
+
# @return [String] Site ID from JustAnalytics dashboard (required).
|
|
16
|
+
attr_accessor :site_id
|
|
17
|
+
|
|
18
|
+
# @return [String] API key from JustAnalytics dashboard (required).
|
|
19
|
+
attr_accessor :api_key
|
|
20
|
+
|
|
21
|
+
# @return [String] Service name, e.g. "rails-api", "worker" (required).
|
|
22
|
+
attr_accessor :service_name
|
|
23
|
+
|
|
24
|
+
# @return [String, nil] Deployment environment, e.g. "production", "staging".
|
|
25
|
+
attr_accessor :environment
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] Release/version string, e.g. "1.2.3", git SHA.
|
|
28
|
+
attr_accessor :release
|
|
29
|
+
|
|
30
|
+
# @return [String] Base URL of JustAnalytics server.
|
|
31
|
+
attr_accessor :server_url
|
|
32
|
+
|
|
33
|
+
# @return [Boolean] Enable debug logging to $stderr (default: false).
|
|
34
|
+
attr_accessor :debug
|
|
35
|
+
|
|
36
|
+
# @return [Float] Flush interval in seconds (default: 2.0).
|
|
37
|
+
attr_accessor :flush_interval
|
|
38
|
+
|
|
39
|
+
# @return [Integer] Maximum items per batch before immediate flush (default: 100).
|
|
40
|
+
attr_accessor :max_batch_size
|
|
41
|
+
|
|
42
|
+
# @return [Boolean] Enable/disable the SDK (default: true).
|
|
43
|
+
attr_accessor :enabled
|
|
44
|
+
|
|
45
|
+
# Default JustAnalytics server URL.
|
|
46
|
+
DEFAULT_SERVER_URL = "https://justanalytics.up.railway.app"
|
|
47
|
+
|
|
48
|
+
def initialize
|
|
49
|
+
@site_id = nil
|
|
50
|
+
@api_key = nil
|
|
51
|
+
@service_name = nil
|
|
52
|
+
@environment = nil
|
|
53
|
+
@release = nil
|
|
54
|
+
@server_url = ENV.fetch("JUSTANALYTICS_URL", DEFAULT_SERVER_URL)
|
|
55
|
+
@debug = false
|
|
56
|
+
@flush_interval = 2.0
|
|
57
|
+
@max_batch_size = 100
|
|
58
|
+
@enabled = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate that all required fields are present.
|
|
62
|
+
#
|
|
63
|
+
# @raise [ArgumentError] if any required field is missing
|
|
64
|
+
# @return [void]
|
|
65
|
+
def validate!
|
|
66
|
+
raise ArgumentError, "[JustAnalytics] init() requires :site_id. Get it from your JustAnalytics dashboard." if site_id.nil? || site_id.to_s.empty?
|
|
67
|
+
raise ArgumentError, "[JustAnalytics] init() requires :api_key. Create one in your JustAnalytics site settings." if api_key.nil? || api_key.to_s.empty?
|
|
68
|
+
raise ArgumentError, '[JustAnalytics] init() requires :service_name (e.g. "rails-api", "worker").' if service_name.nil? || service_name.to_s.empty?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JustAnalytics
|
|
4
|
+
# Thread-local context propagation for distributed tracing.
|
|
5
|
+
#
|
|
6
|
+
# Uses +Thread.current+ for fiber-safe storage. Context is automatically
|
|
7
|
+
# inherited when new fibers are created within a traced scope.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# JustAnalytics::Context.current_span #=> Span or nil
|
|
11
|
+
# JustAnalytics::Context.current_trace_id #=> String or nil
|
|
12
|
+
module Context
|
|
13
|
+
THREAD_KEY = :justanalytics_context
|
|
14
|
+
|
|
15
|
+
# Internal context data structure.
|
|
16
|
+
ContextData = Struct.new(:active_span, :trace_id, :user, :tags, keyword_init: true) do
|
|
17
|
+
def initialize(active_span: nil, trace_id: nil, user: nil, tags: {})
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Get the current context from Thread.current.
|
|
24
|
+
#
|
|
25
|
+
# @return [ContextData, nil]
|
|
26
|
+
def current
|
|
27
|
+
Thread.current[THREAD_KEY]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Set the current context on Thread.current.
|
|
31
|
+
#
|
|
32
|
+
# @param ctx [ContextData, nil]
|
|
33
|
+
# @return [void]
|
|
34
|
+
def current=(ctx)
|
|
35
|
+
Thread.current[THREAD_KEY] = ctx
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the currently active span.
|
|
39
|
+
#
|
|
40
|
+
# @return [Span, nil]
|
|
41
|
+
def current_span
|
|
42
|
+
current&.active_span
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get the current trace ID.
|
|
46
|
+
#
|
|
47
|
+
# @return [String, nil]
|
|
48
|
+
def current_trace_id
|
|
49
|
+
current&.trace_id
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get the current user context.
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash, nil]
|
|
55
|
+
def current_user
|
|
56
|
+
current&.user
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the current tags.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash]
|
|
62
|
+
def current_tags
|
|
63
|
+
current&.tags || {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Set user context on the current scope.
|
|
67
|
+
#
|
|
68
|
+
# @param user [Hash] user context with :id, :email, :username keys
|
|
69
|
+
# @return [void]
|
|
70
|
+
def set_user(user)
|
|
71
|
+
ctx = current
|
|
72
|
+
return unless ctx
|
|
73
|
+
|
|
74
|
+
ctx.user = user
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Set a tag on the current scope.
|
|
78
|
+
#
|
|
79
|
+
# @param key [String] tag key
|
|
80
|
+
# @param value [String] tag value
|
|
81
|
+
# @return [void]
|
|
82
|
+
def set_tag(key, value)
|
|
83
|
+
ctx = current
|
|
84
|
+
return unless ctx
|
|
85
|
+
|
|
86
|
+
# Copy-on-write semantics
|
|
87
|
+
ctx.tags = ctx.tags.merge(key.to_s => value.to_s)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Run a block within a new context scope.
|
|
91
|
+
#
|
|
92
|
+
# Saves the previous context, sets the new one, yields, and
|
|
93
|
+
# restores the previous context afterward (even on exception).
|
|
94
|
+
#
|
|
95
|
+
# @param ctx [ContextData] the context to activate
|
|
96
|
+
# @yield the block to run within the context
|
|
97
|
+
# @return the return value of the block
|
|
98
|
+
def with_context(ctx)
|
|
99
|
+
previous = current
|
|
100
|
+
self.current = ctx
|
|
101
|
+
yield
|
|
102
|
+
ensure
|
|
103
|
+
self.current = previous
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Create a child context inheriting from the current one.
|
|
107
|
+
#
|
|
108
|
+
# @param active_span [Span, nil] the span to set as active
|
|
109
|
+
# @param trace_id [String, nil] trace ID override
|
|
110
|
+
# @return [ContextData]
|
|
111
|
+
def create_child(active_span: nil, trace_id: nil)
|
|
112
|
+
parent = current || ContextData.new
|
|
113
|
+
ContextData.new(
|
|
114
|
+
active_span: active_span || parent.active_span,
|
|
115
|
+
trace_id: trace_id || parent.trace_id,
|
|
116
|
+
user: parent.user,
|
|
117
|
+
tags: parent.tags.dup
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|