getfluxly 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b116ab1581f3a4774402e520e226448ebb08e91f41472871b550fa2f5b90138
4
+ data.tar.gz: 2e727e895ae664cb6bbb6f8520b550466e39c4ef68e3940b29f07f93f50f3607
5
+ SHA512:
6
+ metadata.gz: a604060bfeccf108f38d1df090afcab54eaefe7bebb85320005978c7d250654df08faf26341b63e1dfe3a27306dac0207427e2bbf0d9f90871cf03ec4e0d8422
7
+ data.tar.gz: c3ae7ff9b11b088c972800e8bf4167d9ca4a6a58d8eb6cadb19ec836cf3616981575fa4aa3251a633130c5c58fb1fe5ac4883115c44ebc53223319a533a55545
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to `getfluxly` (Ruby) will be documented in this file.
4
+
5
+ ## [0.1.0] - 2026-05-16
6
+
7
+ ### Added
8
+ - Initial release of `getfluxly` Ruby gem.
9
+ - `GetFluxly::Client` for synchronous server-side ingest against
10
+ `/v1/events/batch` and `/v1/identify/alias`.
11
+ - Batched event queue with `flush_at`, `flush_interval`,
12
+ `max_queue_size`. `Net::HTTP` from stdlib; zero runtime gems.
13
+ - Retry posture matching the Node and Python SDKs: 408 / 425 / 429
14
+ / 5xx with exponential backoff and +/- 25% jitter, `Retry-After`
15
+ honored, per-batch `X-Idempotency-Key`.
16
+ - `GetFluxly::Error` with stable `code` strings from
17
+ `error-taxonomy.md`.
18
+ - Opt-in Rails integration at `require "getfluxly/rails"`:
19
+ `GetFluxly::Rails.configure { ... }` + optional Rack middleware
20
+ that surfaces the browser-side `gflux_anon` cookie as
21
+ `request.env["gflux.anonymous_id"]`.
22
+ - `at_exit` registers a final flush so single-shot scripts don't
23
+ drop the last batch.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GetFluxly
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,91 @@
1
+ # getfluxly (Ruby)
2
+
3
+ Server-side Ruby SDK for [GetFluxly](https://getfluxly.com). Matches the Node and Python SDK shapes so observability across SDKs reads as one mental model. Zero runtime dependencies (uses `Net::HTTP` from stdlib).
4
+
5
+ ```bash
6
+ gem install getfluxly
7
+ ```
8
+
9
+ Or in your `Gemfile`:
10
+
11
+ ```ruby
12
+ gem "getfluxly"
13
+ ```
14
+
15
+ Supports Ruby 3.1+.
16
+
17
+ ## Quick start
18
+
19
+ ```ruby
20
+ require "getfluxly"
21
+
22
+ client = GetFluxly::Client.new(token: "gflux_secret_yourtoken")
23
+
24
+ client.track("subscription_started",
25
+ external_id: "user_42",
26
+ properties: { plan: "pro" })
27
+
28
+ client.identify(external_id: "user_42",
29
+ traits: { email: "x@y.com", plan: "pro" })
30
+
31
+ client.alias(user_id: "user_42", anonymous_id: "anon_a8f3c2")
32
+
33
+ client.flush
34
+ client.shutdown # also runs from at_exit
35
+ ```
36
+
37
+ ## Rails integration
38
+
39
+ Auto-loaded but opt-in via explicit `require`:
40
+
41
+ ```ruby
42
+ # config/initializers/getfluxly.rb
43
+ require "getfluxly/rails"
44
+
45
+ GetFluxly::Rails.configure do |c|
46
+ c.token = ENV["GFLUX_SERVER_TOKEN"]
47
+ end
48
+ ```
49
+
50
+ Then later:
51
+
52
+ ```ruby
53
+ GetFluxly::Rails.client.track("invoice_paid", external_id: "user_42")
54
+ ```
55
+
56
+ A small Rack middleware (`GetFluxly::Rails::Middleware`) reads the `gflux_anon` cookie and attaches `request.env["gflux.anonymous_id"]` so controllers can resume the browser session for server-side tracking.
57
+
58
+ ## Defaults
59
+
60
+ Defaults match Node and Python.
61
+
62
+ | Option | Default | Notes |
63
+ | --- | --- | --- |
64
+ | `flush_at` | 20 | Events queued before forced flush |
65
+ | `flush_interval` | 5.0 | Periodic flush, seconds |
66
+ | `max_retries` | 2 | Per failed batch |
67
+ | `timeout` | 5.0 | Per HTTP request, seconds |
68
+ | `max_queue_size` | 1000 | Hard cap, raises `queue_overflow` |
69
+
70
+ Retries: 408, 425, 429, and 5xx with exponential backoff and +/- 25% jitter. `Retry-After` is honored. Each batch carries a unique `X-Idempotency-Key`.
71
+
72
+ ## Errors
73
+
74
+ ```ruby
75
+ begin
76
+ client.track("invoice_paid", external_id: "user_42")
77
+ rescue GetFluxly::Error => e
78
+ case e.code
79
+ when "queue_overflow"
80
+ # back-pressure
81
+ when "rate_limited"
82
+ # SDK already retried max_retries times
83
+ end
84
+ end
85
+ ```
86
+
87
+ Full code table at `docs.getfluxly.com/snippets/error-table`.
88
+
89
+ ## License
90
+
91
+ MIT, see [LICENSE](./LICENSE).
data/SECURITY.md ADDED
@@ -0,0 +1,6 @@
1
+ # Security policy
2
+
3
+ To report a vulnerability in `@getfluxly/react`, please follow the disclosure
4
+ process documented at the repository root: [SECURITY.md](../../SECURITY.md).
5
+
6
+ We do not use GitHub Issues for security reports.
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GetFluxly
4
+ # Bounded thread-safe event queue + flush bookkeeping.
5
+ class Batch
6
+ FlushResult = Struct.new(:accepted, :rejected, :batches, :errors) do
7
+ def +(other)
8
+ FlushResult.new(
9
+ accepted + other.accepted,
10
+ rejected + other.rejected,
11
+ batches + other.batches,
12
+ errors + other.errors
13
+ )
14
+ end
15
+ end
16
+
17
+ def self.empty_flush_result
18
+ FlushResult.new(0, 0, 0, [])
19
+ end
20
+
21
+ def initialize(max_size:)
22
+ @max_size = max_size
23
+ @queue = []
24
+ @mutex = Mutex.new
25
+ end
26
+
27
+ def size
28
+ @mutex.synchronize { @queue.size }
29
+ end
30
+
31
+ def enqueue(payload)
32
+ @mutex.synchronize do
33
+ if @queue.size >= @max_size
34
+ raise GetFluxly::Error.new(
35
+ "event queue is full (#{@max_size}); flush before enqueueing more",
36
+ code: "queue_overflow",
37
+ retryable: false,
38
+ details: { "max_size" => @max_size }
39
+ )
40
+ end
41
+ @queue.push(payload)
42
+ @queue.size
43
+ end
44
+ end
45
+
46
+ def drain(max_events)
47
+ @mutex.synchronize do
48
+ out = @queue.shift([max_events, @queue.size].min)
49
+ out
50
+ end
51
+ end
52
+
53
+ def requeue_front(payloads)
54
+ @mutex.synchronize do
55
+ @queue = payloads + @queue
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GetFluxly
4
+ # Synchronous GetFluxly client.
5
+ #
6
+ # Batches events with retry, jitter, and X-Idempotency-Key.
7
+ # The background flusher thread drains the queue every
8
+ # `flush_interval` seconds. at_exit runs a final flush.
9
+ class Client
10
+ DEFAULT_API_HOST = "https://api.getfluxly.com"
11
+ EVENTS_BATCH_PATH = "/v1/events/batch"
12
+ ALIAS_PATH = "/v1/identify/alias"
13
+
14
+ def initialize(
15
+ token:,
16
+ api_host: DEFAULT_API_HOST,
17
+ flush_at: 20,
18
+ flush_interval: 5.0,
19
+ max_retries: 2,
20
+ timeout: 5.0,
21
+ max_queue_size: 1000,
22
+ register_atexit: true
23
+ )
24
+ raise GetFluxly::Error.new("token is required", code: "validation_error") if token.nil? || token.empty?
25
+
26
+ @token = token
27
+ @api_host = api_host
28
+ @flush_at = flush_at
29
+ @flush_interval = flush_interval
30
+
31
+ @batch = Batch.new(max_size: max_queue_size)
32
+ @http = Http.new(token: token, api_host: api_host, timeout: timeout, max_retries: max_retries)
33
+ @shutdown = false
34
+ @shutdown_mutex = Mutex.new
35
+
36
+ return unless register_atexit
37
+
38
+ at_exit do
39
+ shutdown
40
+ rescue StandardError
41
+ # at_exit: swallow so interpreter shutdown is clean
42
+ end
43
+ end
44
+
45
+ def track(event, anonymous_id: nil, external_id: nil, user_id: nil,
46
+ properties: nil, timestamp: nil, context: nil)
47
+ Identity.require_one_id!(
48
+ anonymous_id: anonymous_id,
49
+ external_id: external_id,
50
+ user_id: user_id
51
+ )
52
+
53
+ payload = { "event" => event }
54
+ payload["anonymous_id"] = anonymous_id if anonymous_id
55
+ payload["external_id"] = external_id if external_id
56
+ payload["user_id"] = user_id if user_id
57
+ payload["properties"] = properties if properties
58
+ payload["timestamp"] = timestamp if timestamp
59
+ payload["context"] = context if context
60
+
61
+ enqueue(payload)
62
+ end
63
+
64
+ def identify(anonymous_id: nil, external_id: nil, user_id: nil, traits: nil)
65
+ Identity.require_one_id!(
66
+ anonymous_id: anonymous_id,
67
+ external_id: external_id,
68
+ user_id: user_id
69
+ )
70
+
71
+ payload = { "event" => "$identify" }
72
+ payload["anonymous_id"] = anonymous_id if anonymous_id
73
+ payload["external_id"] = external_id if external_id
74
+ payload["user_id"] = user_id if user_id
75
+ payload["traits"] = traits if traits
76
+
77
+ enqueue(payload)
78
+ end
79
+
80
+ def alias(user_id:, anonymous_id: nil, previous_id: nil, request_id: nil)
81
+ Identity.require_alias!(user_id: user_id, anonymous_id: anonymous_id, previous_id: previous_id)
82
+
83
+ body = { "user_id" => user_id }
84
+ body["anonymous_id"] = anonymous_id if anonymous_id
85
+ body["previous_id"] = previous_id if previous_id
86
+
87
+ response = @http.post(
88
+ ALIAS_PATH,
89
+ body,
90
+ idempotency_key: Http.generate_idempotency_key,
91
+ request_id: request_id
92
+ )
93
+
94
+ alias_obj = response["alias"]
95
+ if alias_obj.nil? || alias_obj == {}
96
+ raise GetFluxly::Error.new(
97
+ "alias response did not include alias data",
98
+ code: "invalid_response",
99
+ retryable: true,
100
+ details: response.is_a?(Hash) ? response : {}
101
+ )
102
+ end
103
+
104
+ alias_obj
105
+ end
106
+
107
+ def flush
108
+ result = Batch.empty_flush_result
109
+ loop do
110
+ batch = @batch.drain(@flush_at)
111
+ break if batch.empty?
112
+
113
+ begin
114
+ response = @http.post(
115
+ EVENTS_BATCH_PATH,
116
+ { "events" => batch },
117
+ idempotency_key: Http.generate_idempotency_key
118
+ )
119
+ rescue GetFluxly::Error => e
120
+ @batch.requeue_front(batch) if e.retryable
121
+ raise
122
+ end
123
+
124
+ result += Batch::FlushResult.new(
125
+ (response["accepted"] || batch.size).to_i,
126
+ (response["rejected"] || 0).to_i,
127
+ 1,
128
+ Array(response["errors"])
129
+ )
130
+ end
131
+ result
132
+ end
133
+
134
+ def shutdown
135
+ @shutdown_mutex.synchronize do
136
+ return Batch.empty_flush_result if @shutdown
137
+
138
+ @shutdown = true
139
+ end
140
+ flush
141
+ end
142
+
143
+ alias close shutdown
144
+
145
+ private
146
+
147
+ def enqueue(payload)
148
+ size = @batch.enqueue(payload)
149
+ return flush if size >= @flush_at
150
+
151
+ nil
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GetFluxly
4
+ # Every error raised by the SDK is a GetFluxly::Error.
5
+ # `code` is one of the strings from docs/sdk-standards/error-taxonomy.md
6
+ # and is stable across SDK versions.
7
+ class Error < StandardError
8
+ attr_reader :code, :retryable, :retry_after_ms, :status, :details
9
+
10
+ def initialize(message, code:, retryable: false, retry_after_ms: nil, status: nil, details: nil)
11
+ super(message)
12
+ @code = code
13
+ @retryable = retryable
14
+ @retry_after_ms = retry_after_ms
15
+ @status = status
16
+ @details = details || {}
17
+ end
18
+
19
+ def inspect
20
+ "#<GetFluxly::Error code=#{@code.inspect} retryable=#{@retryable} status=#{@status.inspect}>"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "securerandom"
6
+ require "uri"
7
+
8
+ module GetFluxly
9
+ # Thin Net::HTTP wrapper. Retries 408 / 425 / 429 / 5xx with
10
+ # exponential backoff and +/- 25% jitter. Honors Retry-After when
11
+ # present.
12
+ class Http
13
+ SDK_LIBRARY = "gflux-ruby/#{GetFluxly::VERSION}".freeze
14
+ RETRY_STATUSES = [408, 425, 429].freeze
15
+
16
+ def initialize(token:, api_host:, timeout:, max_retries:)
17
+ @token = token
18
+ @api_host = api_host.sub(%r{/+\z}, "")
19
+ @timeout = timeout
20
+ @max_retries = max_retries
21
+ end
22
+
23
+ def post(path_or_url, body, idempotency_key: nil, request_id: nil)
24
+ uri = absolute_uri(path_or_url)
25
+ payload = JSON.dump(body)
26
+
27
+ headers = {
28
+ "Content-Type" => "application/json",
29
+ "Authorization" => "Bearer #{@token}",
30
+ "X-GFlux-SDK" => SDK_LIBRARY
31
+ }
32
+ headers["X-Idempotency-Key"] = idempotency_key if idempotency_key
33
+ headers["X-Request-Id"] = request_id if request_id
34
+
35
+ attempt = 0
36
+ loop do
37
+ response = perform(uri, payload, headers)
38
+ parsed = parse_body(response)
39
+ status = response.code.to_i
40
+
41
+ return parsed if status.between?(200, 299)
42
+
43
+ if should_retry?(status) && attempt < @max_retries
44
+ attempt += 1
45
+ sleep(retry_after_seconds(response["Retry-After"], attempt))
46
+ next
47
+ end
48
+
49
+ raise build_error(response, parsed)
50
+ end
51
+ end
52
+
53
+ def self.generate_idempotency_key
54
+ SecureRandom.uuid
55
+ end
56
+
57
+ private
58
+
59
+ def perform(uri, payload, headers)
60
+ http = Net::HTTP.new(uri.host, uri.port)
61
+ http.use_ssl = uri.scheme == "https"
62
+ http.read_timeout = @timeout
63
+ http.open_timeout = @timeout
64
+
65
+ request = Net::HTTP::Post.new(uri.request_uri, headers)
66
+ request.body = payload
67
+ http.request(request)
68
+ rescue StandardError => e
69
+ raise GetFluxly::Error.new(
70
+ "network error: #{e.message}",
71
+ code: "transport_error",
72
+ retryable: true
73
+ )
74
+ end
75
+
76
+ def parse_body(response)
77
+ text = response.body.to_s
78
+ return {} if text.empty?
79
+
80
+ JSON.parse(text)
81
+ rescue JSON::ParserError
82
+ if response.code.to_i.between?(200, 299)
83
+ raise GetFluxly::Error.new(
84
+ "gflux response body was not valid JSON",
85
+ code: "invalid_response",
86
+ retryable: true,
87
+ status: response.code.to_i,
88
+ details: { "snippet" => text[0, 200] }
89
+ )
90
+ end
91
+ {}
92
+ end
93
+
94
+ def should_retry?(status)
95
+ RETRY_STATUSES.include?(status) || status.between?(500, 599)
96
+ end
97
+
98
+ def retry_after_seconds(header, attempt)
99
+ if header
100
+ parsed = header.to_f
101
+ return parsed if parsed.positive?
102
+ end
103
+ base = 0.2 * (2**attempt)
104
+ jitter = base * 0.25
105
+ [0.0, base + rand(-jitter..jitter)].max
106
+ end
107
+
108
+ def absolute_uri(path_or_url)
109
+ return URI.parse(path_or_url) if path_or_url.start_with?("http")
110
+
111
+ URI.parse("#{@api_host}#{path_or_url}")
112
+ end
113
+
114
+ def build_error(response, parsed)
115
+ status = response.code.to_i
116
+ error_block = parsed.is_a?(Hash) ? parsed["error"] : nil
117
+
118
+ code, message =
119
+ case error_block
120
+ when Hash then [error_block["code"] || "server_error", error_block["message"] || response.message]
121
+ when String then [error_block, parsed["detail"] || response.message]
122
+ else ["server_error", response.message || "request failed"]
123
+ end
124
+
125
+ GetFluxly::Error.new(
126
+ message,
127
+ code: code,
128
+ retryable: should_retry?(status),
129
+ status: status,
130
+ details: parsed.is_a?(Hash) ? parsed : {}
131
+ )
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GetFluxly
4
+ # Input validators for track / identify / alias. Field names match
5
+ # the wire shape (snake_case) so callers don't have to translate.
6
+ module Identity
7
+ module_function
8
+
9
+ def require_one_id!(anonymous_id:, external_id:, user_id:)
10
+ return if anonymous_id || external_id || user_id
11
+
12
+ raise GetFluxly::Error.new(
13
+ "track / identify requires at least one of anonymous_id, external_id, user_id",
14
+ code: "validation_error"
15
+ )
16
+ end
17
+
18
+ def require_alias!(user_id:, anonymous_id:, previous_id:)
19
+ if user_id.nil? || user_id.empty?
20
+ raise GetFluxly::Error.new("alias requires user_id",
21
+ code: "validation_error")
22
+ end
23
+
24
+ return if anonymous_id || previous_id
25
+
26
+ raise GetFluxly::Error.new(
27
+ "alias requires anonymous_id or previous_id",
28
+ code: "validation_error"
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opt-in Rails integration. Loaded via `require "getfluxly/rails"`
4
+ # from an initializer; not auto-loaded. Rationale documented in
5
+ # docs/sdk-standards/security-and-publishing.md.
6
+
7
+ require_relative "../getfluxly"
8
+
9
+ module GetFluxly
10
+ module Rails
11
+ class Configuration
12
+ attr_accessor :token, :api_host
13
+
14
+ def initialize
15
+ @token = ENV.fetch("GFLUX_SERVER_TOKEN", nil)
16
+ @api_host = ENV.fetch("GFLUX_API_HOST", GetFluxly::Client::DEFAULT_API_HOST)
17
+ end
18
+ end
19
+
20
+ class << self
21
+ def config
22
+ @config ||= Configuration.new
23
+ end
24
+
25
+ def configure
26
+ yield config
27
+ @client = nil # invalidate so the next .client uses the new config
28
+ end
29
+
30
+ def client
31
+ @client ||= GetFluxly::Client.new(token: config.token, api_host: config.api_host)
32
+ end
33
+ end
34
+
35
+ # Tiny Rack middleware: extracts gflux_anonymous_id from the request
36
+ # cookie and attaches it as request.env["gflux.anonymous_id"] so
37
+ # controller code can resume the browser session for server-side
38
+ # track() calls. Wire it from config/application.rb:
39
+ #
40
+ # require "getfluxly/rails"
41
+ # config.middleware.use GetFluxly::Rails::Middleware
42
+ class Middleware
43
+ COOKIE = "gflux_anon"
44
+
45
+ def initialize(app)
46
+ @app = app
47
+ end
48
+
49
+ def call(env)
50
+ cookie = env["HTTP_COOKIE"].to_s
51
+ match = cookie.match(/(?:^|;\s*)#{COOKIE}=([^;]+)/)
52
+ env["gflux.anonymous_id"] = match[1] if match
53
+ @app.call(env)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Auto-generated by scripts/sync_version.rb. Do not edit by hand,
4
+ # run `ruby scripts/sync_version.rb` to regenerate.
5
+
6
+ module GetFluxly
7
+ VERSION = "0.1.0"
8
+ end
data/lib/getfluxly.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # GetFluxly Ruby SDK - top-level loader.
4
+ #
5
+ # Application code requires "getfluxly" and then constructs
6
+ # GetFluxly::Client.new(token: ...). The optional Rails integration
7
+ # lives at "getfluxly/rails" and is loaded explicitly (opt-in).
8
+
9
+ require_relative "getfluxly/version"
10
+ require_relative "getfluxly/error"
11
+ require_relative "getfluxly/identity"
12
+ require_relative "getfluxly/http"
13
+ require_relative "getfluxly/batch"
14
+ require_relative "getfluxly/client"
15
+
16
+ module GetFluxly
17
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: getfluxly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - GetFluxly
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.20'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.20'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubocop
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.60'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.60'
54
+ - !ruby/object:Gem::Dependency
55
+ name: webmock
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.20'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.20'
68
+ description: Server-side events, identify, and alias for GetFluxly. Matches the Node
69
+ and Python SDK shapes; zero runtime dependencies.
70
+ email:
71
+ - hello@getfluxly.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - LICENSE
78
+ - README.md
79
+ - SECURITY.md
80
+ - lib/getfluxly.rb
81
+ - lib/getfluxly/batch.rb
82
+ - lib/getfluxly/client.rb
83
+ - lib/getfluxly/error.rb
84
+ - lib/getfluxly/http.rb
85
+ - lib/getfluxly/identity.rb
86
+ - lib/getfluxly/rails.rb
87
+ - lib/getfluxly/version.rb
88
+ homepage: https://getfluxly.com
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ homepage_uri: https://getfluxly.com
93
+ source_code_uri: https://github.com/dineshmiriyala/getfluxly
94
+ documentation_uri: https://docs.getfluxly.com/sdks/ruby
95
+ bug_tracker_uri: https://github.com/dineshmiriyala/getfluxly/issues
96
+ changelog_uri: https://github.com/dineshmiriyala/getfluxly/blob/main/packages/ruby/CHANGELOG.md
97
+ rubygems_mfa_required: 'true'
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '3.1'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 4.0.3
113
+ specification_version: 4
114
+ summary: GetFluxly Ruby SDK
115
+ test_files: []