klime 1.0.4

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: 330590153e5a8620ea195eeac0af41c0eacbc5d4619f3cf22c05192fcaa071ee
4
+ data.tar.gz: 54cf4a7e4afb4680dcd7386c056058184ac5fb0f46132f980074a115c1f6ac47
5
+ SHA512:
6
+ metadata.gz: b89311ba42ebcc5cf409fbd331e2f77dfcf72455dc2c832a34c9392aaabe216949adedb90f2999cb4a88ae3ede702452db82074694db79a87b33dbe4c5c01a9d
7
+ data.tar.gz: 448ef16a67fc2ce3cf81766bce565cffc60a2ca5cb91dab198cd8a81ca825b643ec5b9c044605e436f56cdbb98ff88a4f9763b507bda1f929b3b05ca101a198a
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Klime
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.
22
+
data/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # klime
2
+
3
+ Klime SDK for Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'klime'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ gem install klime
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```ruby
28
+ require 'klime'
29
+
30
+ client = Klime::Client.new(
31
+ write_key: 'your-write-key'
32
+ )
33
+
34
+ # Identify a user
35
+ client.identify('user_123', {
36
+ email: 'user@example.com',
37
+ name: 'Stefan'
38
+ })
39
+
40
+ # Track an event
41
+ client.track('Button Clicked', {
42
+ button_name: 'Sign up',
43
+ plan: 'pro'
44
+ }, user_id: 'user_123')
45
+
46
+ # Associate user with a group and set group traits
47
+ client.group('org_456', {
48
+ name: 'Acme Inc',
49
+ plan: 'enterprise'
50
+ }, user_id: 'user_123')
51
+
52
+ # Or just link the user to a group (if traits are already set)
53
+ client.group('org_456', user_id: 'user_123')
54
+
55
+ # Shutdown gracefully
56
+ client.shutdown
57
+ ```
58
+
59
+ ## API Reference
60
+
61
+ ### Constructor
62
+
63
+ ```ruby
64
+ Klime::Client.new(
65
+ write_key:, # Required: Your Klime write key
66
+ endpoint: nil, # Optional: API endpoint (default: https://i.klime.com)
67
+ flush_interval: nil, # Optional: Milliseconds between flushes (default: 2000)
68
+ max_batch_size: nil, # Optional: Max events per batch (default: 20, max: 100)
69
+ max_queue_size: nil, # Optional: Max queued events (default: 1000)
70
+ retry_max_attempts: nil, # Optional: Max retry attempts (default: 5)
71
+ retry_initial_delay: nil, # Optional: Initial retry delay in ms (default: 1000)
72
+ flush_on_shutdown: nil # Optional: Auto-flush on exit (default: true)
73
+ )
74
+ ```
75
+
76
+ ### Methods
77
+
78
+ #### `track(event_name, properties = nil, user_id: nil, group_id: nil, ip: nil)`
79
+
80
+ Track a user event. A `user_id` is required for events to be useful in Klime.
81
+
82
+ ```ruby
83
+ client.track('Button Clicked', {
84
+ button_name: 'Sign up',
85
+ plan: 'pro'
86
+ }, user_id: 'user_123')
87
+
88
+ # With IP address (for geolocation)
89
+ client.track('Button Clicked', {
90
+ button_name: 'Sign up',
91
+ plan: 'pro'
92
+ }, user_id: 'user_123', ip: '192.168.1.1')
93
+ ```
94
+
95
+ > **Advanced**: The `group_id` parameter is available for multi-tenant scenarios where a user belongs to multiple organizations and you need to specify which organization context the event occurred in.
96
+
97
+ #### `identify(user_id, traits = nil, ip: nil)`
98
+
99
+ Identify a user with traits.
100
+
101
+ ```ruby
102
+ client.identify('user_123', {
103
+ email: 'user@example.com',
104
+ name: 'Stefan'
105
+ }, ip: '192.168.1.1')
106
+ ```
107
+
108
+ #### `group(group_id, traits = nil, user_id: nil, ip: nil)`
109
+
110
+ Associate a user with a group and/or set group traits.
111
+
112
+ ```ruby
113
+ # Associate user with a group and set group traits (most common)
114
+ client.group('org_456', {
115
+ name: 'Acme Inc',
116
+ plan: 'enterprise'
117
+ }, user_id: 'user_123')
118
+
119
+ # Just link a user to a group (traits already set or not needed)
120
+ client.group('org_456', user_id: 'user_123')
121
+
122
+ # Just update group traits (e.g., from a webhook or background job)
123
+ client.group('org_456', {
124
+ plan: 'enterprise',
125
+ employee_count: 50
126
+ })
127
+ ```
128
+
129
+ #### `flush`
130
+
131
+ Manually flush queued events immediately.
132
+
133
+ ```ruby
134
+ client.flush
135
+ ```
136
+
137
+ #### `shutdown`
138
+
139
+ Gracefully shutdown the client, flushing remaining events.
140
+
141
+ ```ruby
142
+ client.shutdown
143
+ ```
144
+
145
+ ## Features
146
+
147
+ - **Automatic Batching**: Events are automatically batched and sent every 2 seconds or when the batch size reaches 20 events
148
+ - **Automatic Retries**: Failed requests are automatically retried with exponential backoff
149
+ - **Thread-Safe**: Safe to use from multiple threads
150
+ - **Process Exit Handling**: Automatically flushes events on process exit (via `at_exit`)
151
+ - **Zero Dependencies**: Uses only Ruby standard library
152
+
153
+ ## Configuration
154
+
155
+ ### Default Values
156
+
157
+ - `flush_interval`: 2000ms
158
+ - `max_batch_size`: 20 events
159
+ - `max_queue_size`: 1000 events
160
+ - `retry_max_attempts`: 5 attempts
161
+ - `retry_initial_delay`: 1000ms
162
+ - `flush_on_shutdown`: true
163
+
164
+ ## Error Handling
165
+
166
+ The SDK automatically handles:
167
+
168
+ - **Transient errors** (429, 503, network failures): Retries with exponential backoff
169
+ - **Permanent errors** (400, 401): Logs error and drops event
170
+ - **Rate limiting**: Respects `Retry-After` header
171
+
172
+ ## Size Limits
173
+
174
+ - Maximum event size: 200KB
175
+ - Maximum batch size: 10MB
176
+ - Maximum events per batch: 100
177
+
178
+ Events exceeding these limits are rejected and logged.
179
+
180
+ ## Rails Example
181
+
182
+ ```ruby
183
+ # config/initializers/klime.rb
184
+ require 'klime'
185
+
186
+ KLIME = Klime::Client.new(
187
+ write_key: ENV['KLIME_WRITE_KEY']
188
+ )
189
+
190
+ # Ensure graceful shutdown
191
+ at_exit { KLIME.shutdown }
192
+ ```
193
+
194
+ ```ruby
195
+ # app/controllers/buttons_controller.rb
196
+ class ButtonsController < ApplicationController
197
+ def click
198
+ KLIME.track('Button Clicked', {
199
+ button_name: params[:button_name]
200
+ }, user_id: current_user&.id&.to_s, ip: request.remote_ip)
201
+
202
+ render json: { success: true }
203
+ end
204
+ end
205
+ ```
206
+
207
+ ## Sinatra Example
208
+
209
+ ```ruby
210
+ require 'sinatra'
211
+ require 'klime'
212
+
213
+ client = Klime::Client.new(write_key: ENV['KLIME_WRITE_KEY'])
214
+
215
+ post '/api/button-clicked' do
216
+ data = JSON.parse(request.body.read)
217
+
218
+ client.track('Button Clicked', {
219
+ button_name: data['buttonName']
220
+ }, user_id: data['userId'], ip: request.ip)
221
+
222
+ { success: true }.to_json
223
+ end
224
+
225
+ # Graceful shutdown
226
+ at_exit { client.shutdown }
227
+ ```
228
+
229
+ ## Rack Middleware Example
230
+
231
+ ```ruby
232
+ # lib/klime_middleware.rb
233
+ require 'klime'
234
+
235
+ class KlimeMiddleware
236
+ def initialize(app, write_key:)
237
+ @app = app
238
+ @client = Klime::Client.new(write_key: write_key)
239
+ end
240
+
241
+ def call(env)
242
+ status, headers, response = @app.call(env)
243
+
244
+ # Track page views for authenticated users
245
+ user_id = env['rack.session']&.dig('user_id')
246
+ if user_id && env['REQUEST_METHOD'] == 'GET' && status == 200
247
+ @client.track('Page View', {
248
+ path: env['PATH_INFO'],
249
+ method: env['REQUEST_METHOD']
250
+ }, user_id: user_id, ip: env['REMOTE_ADDR'])
251
+ end
252
+
253
+ [status, headers, response]
254
+ end
255
+ end
256
+ ```
257
+
258
+ ## Requirements
259
+
260
+ - Ruby 2.6 or higher
261
+ - No external dependencies (uses only standard library)
262
+
263
+ ## License
264
+
265
+ MIT
@@ -0,0 +1,370 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "thread"
7
+
8
+ module Klime
9
+ # Main client for the Klime analytics SDK
10
+ #
11
+ # @example Basic usage
12
+ # client = Klime::Client.new(write_key: 'your-write-key')
13
+ # client.track('Button Clicked', { button_name: 'Sign up' })
14
+ # client.shutdown
15
+ class Client
16
+ DEFAULT_ENDPOINT = "https://i.klime.com"
17
+ DEFAULT_FLUSH_INTERVAL = 2000 # milliseconds
18
+ DEFAULT_MAX_BATCH_SIZE = 20
19
+ DEFAULT_MAX_QUEUE_SIZE = 1000
20
+ DEFAULT_RETRY_MAX_ATTEMPTS = 5
21
+ DEFAULT_RETRY_INITIAL_DELAY = 1000 # milliseconds
22
+ MAX_BATCH_SIZE = 100
23
+ MAX_EVENT_SIZE_BYTES = 200 * 1024 # 200KB
24
+ MAX_BATCH_SIZE_BYTES = 10 * 1024 * 1024 # 10MB
25
+
26
+ # Create a new Klime client
27
+ #
28
+ # @param write_key [String] Your Klime write key (required)
29
+ # @param endpoint [String] API endpoint URL (default: https://i.klime.com)
30
+ # @param flush_interval [Integer] Milliseconds between automatic flushes (default: 2000)
31
+ # @param max_batch_size [Integer] Maximum events per batch (default: 20, max: 100)
32
+ # @param max_queue_size [Integer] Maximum queued events (default: 1000)
33
+ # @param retry_max_attempts [Integer] Maximum retry attempts (default: 5)
34
+ # @param retry_initial_delay [Integer] Initial retry delay in milliseconds (default: 1000)
35
+ # @param flush_on_shutdown [Boolean] Auto-flush on exit (default: true)
36
+ def initialize(
37
+ write_key:,
38
+ endpoint: nil,
39
+ flush_interval: nil,
40
+ max_batch_size: nil,
41
+ max_queue_size: nil,
42
+ retry_max_attempts: nil,
43
+ retry_initial_delay: nil,
44
+ flush_on_shutdown: nil
45
+ )
46
+ raise ConfigurationError, "write_key is required" if write_key.nil? || write_key.empty?
47
+
48
+ @write_key = write_key
49
+ @endpoint = endpoint || DEFAULT_ENDPOINT
50
+ @flush_interval = flush_interval || DEFAULT_FLUSH_INTERVAL
51
+ @max_batch_size = [max_batch_size || DEFAULT_MAX_BATCH_SIZE, MAX_BATCH_SIZE].min
52
+ @max_queue_size = max_queue_size || DEFAULT_MAX_QUEUE_SIZE
53
+ @retry_max_attempts = retry_max_attempts || DEFAULT_RETRY_MAX_ATTEMPTS
54
+ @retry_initial_delay = retry_initial_delay || DEFAULT_RETRY_INITIAL_DELAY
55
+ @flush_on_shutdown = flush_on_shutdown.nil? ? true : flush_on_shutdown
56
+
57
+ @queue = Queue.new
58
+ @mutex = Mutex.new
59
+ @shutdown = false
60
+ @flush_in_progress = false
61
+ @flush_thread = nil
62
+
63
+ setup_shutdown_hook if @flush_on_shutdown
64
+ schedule_flush
65
+ end
66
+
67
+ # Track a user event
68
+ #
69
+ # @param event_name [String] Name of the event
70
+ # @param properties [Hash] Event properties (optional)
71
+ # @param user_id [String] User ID (optional)
72
+ # @param group_id [String] Group ID (optional)
73
+ # @param ip [String] IP address (optional)
74
+ # @return [void]
75
+ #
76
+ # @example Simple usage
77
+ # client.track('Button Clicked', { button_name: 'Sign up' })
78
+ #
79
+ # @example With user context
80
+ # client.track('Button Clicked', { button_name: 'Sign up' }, user_id: 'user_123', group_id: 'org_456')
81
+ def track(event_name, properties = nil, user_id: nil, group_id: nil, ip: nil)
82
+ return if @shutdown
83
+
84
+ event = Event.new(
85
+ type: EventType::TRACK,
86
+ event: event_name,
87
+ properties: properties || {},
88
+ user_id: user_id,
89
+ group_id: group_id,
90
+ context: build_context(ip)
91
+ )
92
+
93
+ enqueue(event)
94
+ end
95
+
96
+ # Identify a user with traits
97
+ #
98
+ # @param user_id [String] User ID (required)
99
+ # @param traits [Hash] User traits (optional)
100
+ # @param ip [String] IP address (optional)
101
+ # @return [void]
102
+ #
103
+ # @example
104
+ # client.identify('user_123', { email: 'user@example.com', name: 'Stefan' })
105
+ def identify(user_id, traits = nil, ip: nil)
106
+ return if @shutdown
107
+
108
+ event = Event.new(
109
+ type: EventType::IDENTIFY,
110
+ user_id: user_id,
111
+ traits: traits || {},
112
+ context: build_context(ip)
113
+ )
114
+
115
+ enqueue(event)
116
+ end
117
+
118
+ # Associate a user with a group and set group traits
119
+ #
120
+ # @param group_id [String] Group ID (required)
121
+ # @param traits [Hash] Group traits (optional)
122
+ # @param user_id [String] User ID (optional)
123
+ # @param ip [String] IP address (optional)
124
+ # @return [void]
125
+ #
126
+ # @example Simple usage
127
+ # client.group('org_456', { name: 'Acme Inc', plan: 'enterprise' })
128
+ #
129
+ # @example With user ID
130
+ # client.group('org_456', { name: 'Acme Inc' }, user_id: 'user_123')
131
+ def group(group_id, traits = nil, user_id: nil, ip: nil)
132
+ return if @shutdown
133
+
134
+ event = Event.new(
135
+ type: EventType::GROUP,
136
+ group_id: group_id,
137
+ user_id: user_id,
138
+ traits: traits || {},
139
+ context: build_context(ip)
140
+ )
141
+
142
+ enqueue(event)
143
+ end
144
+
145
+ # Manually flush queued events immediately
146
+ #
147
+ # @return [void]
148
+ def flush
149
+ return if @shutdown
150
+
151
+ @mutex.synchronize do
152
+ return if @flush_in_progress
153
+
154
+ @flush_in_progress = true
155
+ end
156
+
157
+ begin
158
+ do_flush
159
+ ensure
160
+ @mutex.synchronize do
161
+ @flush_in_progress = false
162
+ end
163
+ end
164
+ end
165
+
166
+ # Gracefully shutdown the client, flushing remaining events
167
+ #
168
+ # @return [void]
169
+ def shutdown
170
+ return if @shutdown
171
+
172
+ @shutdown = true
173
+
174
+ # Cancel scheduled flush
175
+ @flush_thread&.kill
176
+
177
+ # Force flush remaining events (bypass normal flush which checks @shutdown)
178
+ flush_remaining
179
+ end
180
+
181
+ private
182
+
183
+ def setup_shutdown_hook
184
+ at_exit { shutdown }
185
+ end
186
+
187
+ def schedule_flush
188
+ return if @shutdown
189
+
190
+ @flush_thread = Thread.new do
191
+ sleep(@flush_interval / 1000.0)
192
+ flush unless @shutdown
193
+ schedule_flush unless @shutdown
194
+ end
195
+ end
196
+
197
+ def enqueue(event)
198
+ # Check event size
199
+ event_size = estimate_event_size(event)
200
+ if event_size > MAX_EVENT_SIZE_BYTES
201
+ warn "Klime: Event size (#{event_size} bytes) exceeds #{MAX_EVENT_SIZE_BYTES} bytes limit"
202
+ return
203
+ end
204
+
205
+ # Drop oldest if queue is full
206
+ @queue.pop(true) if @queue.size >= @max_queue_size rescue nil
207
+
208
+ @queue.push(event)
209
+
210
+ # Check if we should flush immediately
211
+ flush if @queue.size >= @max_batch_size
212
+ end
213
+
214
+ def do_flush
215
+ # Cancel any scheduled flush thread
216
+ @flush_thread&.kill
217
+
218
+ # Process batches
219
+ flush_remaining
220
+
221
+ # Schedule next flush
222
+ schedule_flush unless @shutdown
223
+ end
224
+
225
+ # Flush all queued events without scheduling (used by shutdown)
226
+ def flush_remaining
227
+ loop do
228
+ batch = extract_batch
229
+ break if batch.empty?
230
+
231
+ send_batch(batch)
232
+ end
233
+ end
234
+
235
+ def extract_batch
236
+ batch = []
237
+ batch_size = 0
238
+
239
+ while batch.length < MAX_BATCH_SIZE && batch.length < @max_batch_size
240
+ begin
241
+ event = @queue.pop(true) # non-blocking
242
+ rescue ThreadError
243
+ break # Queue is empty
244
+ end
245
+
246
+ event_size = estimate_event_size(event)
247
+
248
+ # Check if adding this event would exceed batch size limit
249
+ if batch_size + event_size > MAX_BATCH_SIZE_BYTES
250
+ # Put event back (at the front would be ideal, but Queue doesn't support that)
251
+ # We'll lose some ordering but it's acceptable for analytics
252
+ @queue.push(event)
253
+ break
254
+ end
255
+
256
+ batch << event
257
+ batch_size += event_size
258
+ end
259
+
260
+ batch
261
+ end
262
+
263
+ def send_batch(batch)
264
+ return if batch.empty?
265
+
266
+ request_body = { batch: batch.map(&:to_h) }.to_json
267
+ uri = URI.parse("#{@endpoint}/v1/batch")
268
+
269
+ attempt = 0
270
+ delay = @retry_initial_delay / 1000.0 # Convert to seconds
271
+
272
+ while attempt < @retry_max_attempts
273
+ begin
274
+ response = make_request(uri, request_body)
275
+
276
+ case response
277
+ when Net::HTTPSuccess
278
+ data = JSON.parse(response.body)
279
+ batch_response = BatchResponse.new(
280
+ status: data["status"] || "ok",
281
+ accepted: data["accepted"] || 0,
282
+ failed: data["failed"] || 0,
283
+ errors: parse_errors(data["errors"])
284
+ )
285
+
286
+ if batch_response.failed > 0 && batch_response.errors
287
+ warn "Klime: Batch partially failed. Accepted: #{batch_response.accepted}, Failed: #{batch_response.failed}"
288
+ end
289
+ return
290
+
291
+ when Net::HTTPBadRequest, Net::HTTPUnauthorized
292
+ data = JSON.parse(response.body) rescue {}
293
+ warn "Klime: Permanent error (#{response.code}): #{data}"
294
+ return
295
+
296
+ when Net::HTTPTooManyRequests, Net::HTTPServiceUnavailable
297
+ retry_after = response["Retry-After"]
298
+ delay = retry_after.to_i if retry_after && retry_after.to_i > 0
299
+
300
+ attempt += 1
301
+ if attempt < @retry_max_attempts
302
+ sleep(delay)
303
+ delay = [delay * 2, 16.0].min
304
+ next
305
+ end
306
+
307
+ else
308
+ # Other errors - retry
309
+ attempt += 1
310
+ if attempt < @retry_max_attempts
311
+ sleep(delay)
312
+ delay = [delay * 2, 16.0].min
313
+ end
314
+ end
315
+
316
+ rescue StandardError => e
317
+ # Network errors - retry
318
+ attempt += 1
319
+ if attempt < @retry_max_attempts
320
+ sleep(delay)
321
+ delay = [delay * 2, 16.0].min
322
+ else
323
+ warn "Klime: Failed to send batch after retries: #{e.message}"
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ def make_request(uri, body)
330
+ http = Net::HTTP.new(uri.host, uri.port)
331
+ http.use_ssl = uri.scheme == "https"
332
+ http.open_timeout = 10
333
+ http.read_timeout = 10
334
+
335
+ request = Net::HTTP::Post.new(uri.request_uri)
336
+ request["Content-Type"] = "application/json"
337
+ request["Authorization"] = "Bearer #{@write_key}"
338
+ request.body = body
339
+
340
+ http.request(request)
341
+ end
342
+
343
+ def build_context(ip = nil)
344
+ context = EventContext.new(
345
+ library: LibraryInfo.new(name: "ruby-sdk", version: VERSION)
346
+ )
347
+ context.ip = ip if ip
348
+ context
349
+ end
350
+
351
+ def estimate_event_size(event)
352
+ event.to_json.bytesize
353
+ rescue StandardError
354
+ 500
355
+ end
356
+
357
+ def parse_errors(errors)
358
+ return nil unless errors.is_a?(Array)
359
+
360
+ errors.map do |err|
361
+ ValidationError.new(
362
+ index: err["index"] || -1,
363
+ message: err["message"] || "",
364
+ code: err["code"] || ""
365
+ )
366
+ end
367
+ end
368
+ end
369
+ end
370
+
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+ require "json"
6
+
7
+ module Klime
8
+ # Event types supported by the API
9
+ module EventType
10
+ TRACK = "track"
11
+ IDENTIFY = "identify"
12
+ GROUP = "group"
13
+ end
14
+
15
+ # Library information included in event context
16
+ class LibraryInfo
17
+ attr_reader :name, :version
18
+
19
+ def initialize(name:, version:)
20
+ @name = name
21
+ @version = version
22
+ end
23
+
24
+ def to_h
25
+ {
26
+ name: @name,
27
+ version: @version
28
+ }
29
+ end
30
+ end
31
+
32
+ # Context information for events
33
+ class EventContext
34
+ attr_reader :library
35
+ attr_accessor :ip
36
+
37
+ def initialize(library: nil, ip: nil)
38
+ @library = library
39
+ @ip = ip
40
+ end
41
+
42
+ def to_h
43
+ result = {}
44
+ result[:library] = @library.to_h if @library
45
+ result[:ip] = @ip if @ip
46
+ result
47
+ end
48
+ end
49
+
50
+ # Base event class
51
+ class Event
52
+ attr_reader :type, :message_id, :timestamp, :context
53
+ attr_accessor :event, :user_id, :group_id, :properties, :traits
54
+
55
+ def initialize(
56
+ type:,
57
+ message_id: nil,
58
+ timestamp: nil,
59
+ event: nil,
60
+ user_id: nil,
61
+ group_id: nil,
62
+ properties: nil,
63
+ traits: nil,
64
+ context: nil
65
+ )
66
+ @type = type
67
+ @message_id = message_id || SecureRandom.uuid
68
+ @timestamp = timestamp || Time.now.utc.iso8601(3)
69
+ @event = event
70
+ @user_id = user_id
71
+ @group_id = group_id
72
+ @properties = properties
73
+ @traits = traits
74
+ @context = context
75
+ end
76
+
77
+ def to_h
78
+ result = {
79
+ type: @type,
80
+ messageId: @message_id,
81
+ timestamp: @timestamp
82
+ }
83
+
84
+ result[:event] = @event if @event
85
+ result[:userId] = @user_id if @user_id
86
+ result[:groupId] = @group_id if @group_id
87
+ result[:properties] = @properties if @properties
88
+ result[:traits] = @traits if @traits
89
+ result[:context] = @context.to_h if @context && !@context.to_h.empty?
90
+
91
+ result
92
+ end
93
+
94
+ def to_json(*args)
95
+ to_h.to_json(*args)
96
+ end
97
+ end
98
+
99
+ # Validation error from batch response
100
+ class ValidationError
101
+ attr_reader :index, :message, :code
102
+
103
+ def initialize(index:, message:, code:)
104
+ @index = index
105
+ @message = message
106
+ @code = code
107
+ end
108
+ end
109
+
110
+ # Batch response from the API
111
+ class BatchResponse
112
+ attr_reader :status, :accepted, :failed, :errors
113
+
114
+ def initialize(status:, accepted:, failed:, errors: nil)
115
+ @status = status
116
+ @accepted = accepted
117
+ @failed = failed
118
+ @errors = errors
119
+ end
120
+ end
121
+ end
122
+
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Klime
4
+ VERSION = "1.0.4"
5
+ end
6
+
data/lib/klime.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "klime/version"
4
+ require_relative "klime/event"
5
+ require_relative "klime/client"
6
+
7
+ module Klime
8
+ class Error < StandardError; end
9
+ class ConfigurationError < Error; end
10
+ end
11
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: klime
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Klime
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-12-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Track events, identify users, and group them with organizations using
56
+ Klime analytics.
57
+ email:
58
+ - support@klime.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE.md
64
+ - README.md
65
+ - lib/klime.rb
66
+ - lib/klime/client.rb
67
+ - lib/klime/event.rb
68
+ - lib/klime/version.rb
69
+ homepage: https://github.com/klimeapp/klime-ruby
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ homepage_uri: https://github.com/klimeapp/klime-ruby
74
+ source_code_uri: https://github.com/klimeapp/klime-ruby
75
+ documentation_uri: https://github.com/klimeapp/klime-ruby
76
+ changelog_uri: https://github.com/klimeapp/klime-ruby/blob/main/CHANGELOG.md
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.6.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.0.3.1
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Klime SDK for Ruby
96
+ test_files: []