doow_track 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: be141768a653090f70f87d0970d3e5bfbfbfcb58379b98ac45e90c144ee77eb9
4
+ data.tar.gz: d8582db5050a500a37fa1e747b2d84a72ac832b3e97e1ade7146081637639365
5
+ SHA512:
6
+ metadata.gz: c2781c02faae7ee84799985834197da9c84fbe0bae637b8dc326e038d79d7d6938348a7b2db94bf8f1453c645fdc8d905049c6d46f31cfc30c7d627683aafa5a
7
+ data.tar.gz: 076d1ff26f3214a496ec16b5ce764e72a362582487d8fa7971b3f6dda9f306eae5509310b7eb18013bd7e35d3eebb98fddffc1673cd2af4a267b4d272544e7ad
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Doow
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,150 @@
1
+ # Doow Track Ruby SDK
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/doow_track.svg)](https://rubygems.org/gems/doow_track)
4
+ [![Ruby](https://img.shields.io/badge/Ruby-3.0+-red)](https://www.ruby-lang.org/)
5
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ Official Ruby SDK for [Doow](https://doow.co) usage telemetry and management.
8
+
9
+ ## Features
10
+
11
+ | Feature | Description |
12
+ |---------|-------------|
13
+ | **Ruby 3.0+** | Modern Ruby with pattern matching and endless methods |
14
+ | **Batching** | Events queued and sent in configurable batches |
15
+ | **Compression** | Automatic gzip for payloads >1KB |
16
+ | **Retries** | Exponential backoff with configurable retry count |
17
+ | **Type Safety** | Struct-based models |
18
+ | **Sidecar** | Executable for stdin/file/tcp input modes |
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```ruby
25
+ gem 'doow_track'
26
+ ```
27
+
28
+ Or install directly:
29
+
30
+ ```bash
31
+ gem install doow_track
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ ### Track Usage Events
39
+
40
+ ```ruby
41
+ require 'doow_track'
42
+
43
+ tracker = DoowTrack::Tracker.new('dk_your_api_key')
44
+
45
+ # Simple event
46
+ tracker.track(DoowTrack::TrackEvent.new(
47
+ metric: 'api_calls',
48
+ quantity: 1,
49
+ license_id: 'lic_abc123'
50
+ ))
51
+
52
+ # Rich event with metadata
53
+ tracker.track(DoowTrack::TrackEvent.new(
54
+ metric: 'tokens_generated',
55
+ quantity: 1500,
56
+ license_id: 'lic_abc123',
57
+ unit: 'tokens',
58
+ attribution: { model: 'gpt-4', region: 'us-east-1' },
59
+ metadata: { request_id: 'req_xyz' }
60
+ ))
61
+
62
+ tracker.shutdown
63
+ ```
64
+
65
+ ### Management API
66
+
67
+ ```ruby
68
+ require 'doow_track'
69
+
70
+ mgmt = DoowTrack::Management.new('dk_your_api_key')
71
+
72
+ # Create an app
73
+ app = mgmt.apps.create(
74
+ name: 'My SaaS Platform',
75
+ description: 'Usage-based billing'
76
+ )
77
+ puts "Created app: #{app.id}"
78
+
79
+ # Create a contract with license
80
+ contract = mgmt.contracts.create(app.id,
81
+ title: 'Enterprise Plan',
82
+ contract_type: DoowTrack::ContractType::PAY_AS_YOU_GO,
83
+ licenses: [
84
+ { name: 'API Usage', license_type: DoowTrack::LicenseType::USAGE_BASED }
85
+ ]
86
+ )
87
+
88
+ license_id = contract.licenses.first.id
89
+
90
+ # Create a metric
91
+ metric = mgmt.metrics.create(license_id, metric_type: 'api_calls')
92
+ puts "Created metric: #{metric.id}"
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Configuration
98
+
99
+ ### Tracker Options
100
+
101
+ ```ruby
102
+ tracker = DoowTrack::Tracker.new('dk_your_api_key',
103
+ endpoint: 'https://api.doow.co',
104
+ enabled: true,
105
+ debug: true,
106
+ flush_at: 20,
107
+ flush_interval: 10,
108
+ max_queue_size: 10_000,
109
+ timeout: 10,
110
+ retry_count: 3,
111
+ disable_compression: false,
112
+ attribution: { service: 'api-gateway' },
113
+ on_error: ->(e) { Rails.logger.error("Tracker error: #{e.message}") }
114
+ )
115
+ ```
116
+
117
+ ### Environment Variables
118
+
119
+ | Variable | Description | Default |
120
+ |----------|-------------|---------|
121
+ | `DOOW_TRACK_API_KEY` | API key (overrides constructor) | — |
122
+ | `DOOW_TRACK_ENDPOINT` | Custom API endpoint | `https://api.doow.co` |
123
+ | `DOOW_TRACK_DISABLED` | Set `true` to disable tracking | `false` |
124
+ | `DOOW_TRACK_DEBUG` | Set `true` for debug logs | `false` |
125
+
126
+ ---
127
+
128
+ ## Error Handling
129
+
130
+ ```ruby
131
+ begin
132
+ app = mgmt.apps.get('invalid_id')
133
+ rescue DoowTrack::Error => e
134
+ if e.not_found?
135
+ puts "App not found"
136
+ elsif e.unauthorized?
137
+ puts "Invalid API key"
138
+ elsif e.rate_limited?
139
+ puts "Rate limited"
140
+ else
141
+ puts "Error: #{e.message}"
142
+ end
143
+ end
144
+ ```
145
+
146
+ ---
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoowTrack
4
+ class Error < StandardError
5
+ attr_reader :status_code, :error_class
6
+
7
+ def initialize(message, status_code: 0, error_class: nil)
8
+ super(message)
9
+ @status_code = status_code
10
+ @error_class = error_class
11
+ end
12
+
13
+ def not_found?
14
+ status_code == 404
15
+ end
16
+
17
+ def unauthorized?
18
+ status_code == 401
19
+ end
20
+
21
+ def forbidden?
22
+ status_code == 403
23
+ end
24
+
25
+ def rate_limited?
26
+ status_code == 429
27
+ end
28
+
29
+ def server_error?
30
+ status_code >= 500
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module DoowTrack
8
+ class Management
9
+ DEFAULT_OPTIONS = {
10
+ endpoint: "https://api.doow.co",
11
+ timeout: 30,
12
+ retry_count: 3,
13
+ debug: false
14
+ }.freeze
15
+
16
+ def initialize(api_key, **options)
17
+ @api_key = ENV["DOOW_TRACK_API_KEY"] || api_key
18
+ @options = DEFAULT_OPTIONS.merge(options)
19
+ @options[:endpoint] = ENV["DOOW_TRACK_ENDPOINT"] if ENV["DOOW_TRACK_ENDPOINT"]
20
+ @options[:debug] = true if ENV["DOOW_TRACK_DEBUG"] == "true"
21
+
22
+ raise Error, "Invalid API key format. Must start with 'dk_'." unless @api_key&.start_with?("dk_")
23
+ end
24
+
25
+ def apps
26
+ AppsResource.new(self)
27
+ end
28
+
29
+ def contracts
30
+ ContractsResource.new(self)
31
+ end
32
+
33
+ def licenses
34
+ LicensesResource.new(self)
35
+ end
36
+
37
+ def metrics
38
+ MetricsResource.new(self)
39
+ end
40
+
41
+ def expenses
42
+ ExpensesResource.new(self)
43
+ end
44
+
45
+ def request(method, path, body: nil)
46
+ url = URI("#{@options[:endpoint].chomp('/')}/sdk#{path}")
47
+ log("[doow/management] #{method.upcase} #{path}")
48
+
49
+ (@options[:retry_count] + 1).times do |attempt|
50
+ begin
51
+ http = Net::HTTP.new(url.host, url.port)
52
+ http.use_ssl = url.scheme == "https"
53
+ http.open_timeout = @options[:timeout]
54
+ http.read_timeout = @options[:timeout]
55
+
56
+ request = case method
57
+ when :get then Net::HTTP::Get.new(url)
58
+ when :post then Net::HTTP::Post.new(url)
59
+ when :patch then Net::HTTP::Patch.new(url)
60
+ when :delete then Net::HTTP::Delete.new(url)
61
+ end
62
+
63
+ request["Authorization"] = "Bearer #{@api_key}"
64
+ request["Content-Type"] = "application/json"
65
+ request.body = body.to_json if body
66
+
67
+ response = http.request(request)
68
+
69
+ if response.code.to_i >= 200 && response.code.to_i < 300
70
+ return nil if response.body.nil? || response.body.empty?
71
+ return JSON.parse(response.body, symbolize_names: true)
72
+ end
73
+
74
+ if response.code.to_i >= 500 && attempt < @options[:retry_count]
75
+ sleep(2**attempt)
76
+ next
77
+ end
78
+
79
+ raise Error.new("API error: #{response.body}", status_code: response.code.to_i)
80
+ rescue StandardError => e
81
+ raise if e.is_a?(Error)
82
+ if attempt < @options[:retry_count]
83
+ sleep(2**attempt)
84
+ next
85
+ end
86
+ raise Error.new("Request failed: #{e.message}")
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def log(message)
94
+ $stderr.puts(message) if @options[:debug]
95
+ end
96
+ end
97
+
98
+ class AppsResource
99
+ def initialize(mgmt)
100
+ @mgmt = mgmt
101
+ end
102
+
103
+ def list(cursor: nil, limit: 50)
104
+ path = "/apps?limit=#{limit}"
105
+ path += "&cursor=#{cursor}" if cursor
106
+ data = @mgmt.request(:get, path)
107
+ parse_paginated(data, App)
108
+ end
109
+
110
+ def get(id)
111
+ data = @mgmt.request(:get, "/apps/#{id}")
112
+ App.new(**data)
113
+ end
114
+
115
+ def create(name:, **attrs)
116
+ data = @mgmt.request(:post, "/apps", body: { name: name, **attrs })
117
+ App.new(**data)
118
+ end
119
+
120
+ def update(id, **attrs)
121
+ data = @mgmt.request(:patch, "/apps/#{id}", body: attrs)
122
+ App.new(**data)
123
+ end
124
+
125
+ def delete(id)
126
+ @mgmt.request(:delete, "/apps/#{id}")
127
+ end
128
+
129
+ private
130
+
131
+ def parse_paginated(data, klass)
132
+ PaginatedResponse.new(
133
+ data: data[:data].map { |d| klass.new(**d) },
134
+ next_cursor: data[:next_cursor],
135
+ has_more: data[:has_more]
136
+ )
137
+ end
138
+ end
139
+
140
+ class ContractsResource
141
+ def initialize(mgmt)
142
+ @mgmt = mgmt
143
+ end
144
+
145
+ def list_by_app(app_id, cursor: nil, limit: 50)
146
+ path = "/apps/#{app_id}/contracts?limit=#{limit}"
147
+ path += "&cursor=#{cursor}" if cursor
148
+ data = @mgmt.request(:get, path)
149
+ parse_paginated(data)
150
+ end
151
+
152
+ def get(id)
153
+ data = @mgmt.request(:get, "/contracts/#{id}")
154
+ parse_contract(data)
155
+ end
156
+
157
+ def create(app_id, title:, contract_type: ContractType::PAY_AS_YOU_GO, licenses: [])
158
+ data = @mgmt.request(:post, "/apps/#{app_id}/contracts", body: {
159
+ title: title,
160
+ contract_type: contract_type,
161
+ licenses: licenses
162
+ })
163
+ parse_contract(data)
164
+ end
165
+
166
+ def delete(id)
167
+ @mgmt.request(:delete, "/contracts/#{id}")
168
+ end
169
+
170
+ private
171
+
172
+ def parse_contract(data)
173
+ licenses = (data[:licenses] || []).map { |l| License.new(**l) }
174
+ Contract.new(**data.merge(licenses: licenses))
175
+ end
176
+
177
+ def parse_paginated(data)
178
+ PaginatedResponse.new(
179
+ data: data[:data].map { |d| parse_contract(d) },
180
+ next_cursor: data[:next_cursor],
181
+ has_more: data[:has_more]
182
+ )
183
+ end
184
+ end
185
+
186
+ class LicensesResource
187
+ def initialize(mgmt)
188
+ @mgmt = mgmt
189
+ end
190
+
191
+ def list(cursor: nil, limit: 50)
192
+ path = "/licenses?limit=#{limit}"
193
+ path += "&cursor=#{cursor}" if cursor
194
+ data = @mgmt.request(:get, path)
195
+ PaginatedResponse.new(
196
+ data: data[:data].map { |d| License.new(**d) },
197
+ next_cursor: data[:next_cursor],
198
+ has_more: data[:has_more]
199
+ )
200
+ end
201
+
202
+ def get(id)
203
+ data = @mgmt.request(:get, "/licenses/#{id}")
204
+ License.new(**data)
205
+ end
206
+ end
207
+
208
+ class MetricsResource
209
+ def initialize(mgmt)
210
+ @mgmt = mgmt
211
+ end
212
+
213
+ def list_by_license(license_id, cursor: nil, limit: 50)
214
+ path = "/licenses/#{license_id}/metrics?limit=#{limit}"
215
+ path += "&cursor=#{cursor}" if cursor
216
+ data = @mgmt.request(:get, path)
217
+ PaginatedResponse.new(
218
+ data: data[:data].map { |d| Metric.new(**d) },
219
+ next_cursor: data[:next_cursor],
220
+ has_more: data[:has_more]
221
+ )
222
+ end
223
+
224
+ def get(id)
225
+ data = @mgmt.request(:get, "/metrics/#{id}")
226
+ Metric.new(**data)
227
+ end
228
+
229
+ def create(license_id, metric_type:, **attrs)
230
+ data = @mgmt.request(:post, "/licenses/#{license_id}/metrics", body: { metric_type: metric_type, **attrs })
231
+ Metric.new(**data)
232
+ end
233
+
234
+ def delete(id)
235
+ @mgmt.request(:delete, "/metrics/#{id}")
236
+ end
237
+ end
238
+
239
+ class ExpensesResource
240
+ def initialize(mgmt)
241
+ @mgmt = mgmt
242
+ end
243
+
244
+ def list_by_app(app_id, cursor: nil, limit: 50)
245
+ path = "/apps/#{app_id}/expenses?limit=#{limit}"
246
+ path += "&cursor=#{cursor}" if cursor
247
+ data = @mgmt.request(:get, path)
248
+ PaginatedResponse.new(
249
+ data: data[:data].map { |d| Expense.new(**d) },
250
+ next_cursor: data[:next_cursor],
251
+ has_more: data[:has_more]
252
+ )
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "zlib"
6
+ require "uri"
7
+
8
+ module DoowTrack
9
+ class Tracker
10
+ DEFAULT_OPTIONS = {
11
+ endpoint: "https://api.doow.co",
12
+ enabled: true,
13
+ debug: false,
14
+ flush_at: 20,
15
+ flush_interval: 10,
16
+ max_queue_size: 10_000,
17
+ timeout: 10,
18
+ retry_count: 3,
19
+ disable_compression: false,
20
+ attribution: nil,
21
+ on_error: nil
22
+ }.freeze
23
+
24
+ def initialize(api_key, **options)
25
+ @api_key = ENV["DOOW_TRACK_API_KEY"] || api_key
26
+ @options = DEFAULT_OPTIONS.merge(options)
27
+ @options[:endpoint] = ENV["DOOW_TRACK_ENDPOINT"] if ENV["DOOW_TRACK_ENDPOINT"]
28
+ @options[:enabled] = false if ENV["DOOW_TRACK_DISABLED"] == "true"
29
+ @options[:debug] = true if ENV["DOOW_TRACK_DEBUG"] == "true"
30
+
31
+ raise Error, "Invalid API key format. Must start with 'dk_'." unless @api_key&.start_with?("dk_")
32
+
33
+ @buffer = []
34
+ @mutex = Mutex.new
35
+ @flusher = start_flusher
36
+ end
37
+
38
+ def track(event)
39
+ return unless @options[:enabled]
40
+
41
+ final_event = event.is_a?(Hash) ? TrackEvent.new(**event) : event
42
+ final_event = TrackEvent.new(
43
+ **final_event.to_h.merge(
44
+ timestamp: final_event.timestamp || Time.now.utc,
45
+ attribution: merge_attribution(final_event.attribution)
46
+ )
47
+ )
48
+
49
+ @mutex.synchronize do
50
+ if @buffer.size >= @options[:max_queue_size]
51
+ log("[doow-track] Queue full, dropping event")
52
+ return
53
+ end
54
+ @buffer << final_event
55
+ flush_async if @buffer.size >= @options[:flush_at]
56
+ end
57
+ end
58
+
59
+ def flush
60
+ batch = nil
61
+ @mutex.synchronize do
62
+ return if @buffer.empty?
63
+ batch = @buffer.dup
64
+ @buffer.clear
65
+ end
66
+ send_batch(batch) if batch
67
+ end
68
+
69
+ def shutdown
70
+ @flusher&.kill
71
+ flush
72
+ end
73
+
74
+ private
75
+
76
+ def start_flusher
77
+ return nil if @options[:flush_interval] <= 0
78
+
79
+ Thread.new do
80
+ loop do
81
+ sleep @options[:flush_interval]
82
+ flush
83
+ end
84
+ end
85
+ end
86
+
87
+ def flush_async
88
+ Thread.new { flush }
89
+ end
90
+
91
+ def send_batch(batch)
92
+ url = URI("#{@options[:endpoint].chomp('/')}/telemetry/events")
93
+ payload = { events: batch.map(&:to_h) }
94
+ json = payload.to_json
95
+
96
+ (@options[:retry_count] + 1).times do |attempt|
97
+ begin
98
+ http = Net::HTTP.new(url.host, url.port)
99
+ http.use_ssl = url.scheme == "https"
100
+ http.open_timeout = @options[:timeout]
101
+ http.read_timeout = @options[:timeout]
102
+
103
+ request = Net::HTTP::Post.new(url)
104
+ request["Authorization"] = "Bearer #{@api_key}"
105
+ request["Content-Type"] = "application/json"
106
+
107
+ body = json
108
+ if !@options[:disable_compression] && json.bytesize > 1024
109
+ body = Zlib::Deflate.deflate(json, Zlib::DEFAULT_COMPRESSION)
110
+ request["Content-Encoding"] = "gzip"
111
+ end
112
+ request.body = body
113
+
114
+ response = http.request(request)
115
+
116
+ if response.code.to_i >= 200 && response.code.to_i < 300
117
+ log("[doow-track] Flushed #{batch.size} events")
118
+ return
119
+ end
120
+
121
+ if response.code.to_i >= 500 && attempt < @options[:retry_count]
122
+ sleep(2**attempt)
123
+ next
124
+ end
125
+
126
+ raise Error.new("API error: #{response.body}", status_code: response.code.to_i)
127
+ rescue StandardError => e
128
+ if attempt < @options[:retry_count]
129
+ sleep(2**attempt)
130
+ next
131
+ end
132
+ @options[:on_error]&.call(e)
133
+ log("[doow-track] Error: #{e.message}")
134
+ end
135
+ end
136
+ end
137
+
138
+ def merge_attribution(event_attribution)
139
+ return event_attribution if @options[:attribution].nil?
140
+ return @options[:attribution] if event_attribution.nil?
141
+ @options[:attribution].merge(event_attribution)
142
+ end
143
+
144
+ def log(message)
145
+ $stderr.puts(message) if @options[:debug]
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoowTrack
4
+ module EventKind
5
+ USAGE = "USAGE"
6
+ ADJUSTMENT = "ADJUSTMENT"
7
+ end
8
+
9
+ module ContractType
10
+ PAY_AS_YOU_GO = "PAY_AS_YOU_GO"
11
+ ENTERPRISE = "ENTERPRISE"
12
+ UNKNOWN = "UNKNOWN"
13
+ end
14
+
15
+ # License types for SDK creation. SDK only supports USAGE_BASED.
16
+ module LicenseType
17
+ USAGE_BASED = "USAGE_BASED"
18
+ end
19
+
20
+ # All license types (for responses that may include UI-created licenses)
21
+ module AllLicenseType
22
+ USAGE_BASED = "USAGE_BASED"
23
+ SEAT_BASED = "SEAT_BASED"
24
+ PREPAID_CREDITS = "PREPAID_CREDITS"
25
+ FLAT_RATE = "FLAT_RATE"
26
+ end
27
+
28
+ module UsageAggregationType
29
+ SUM = "SUM"
30
+ MAX = "MAX"
31
+ CUMULATIVE = "CUMULATIVE"
32
+ end
33
+
34
+ module RateKind
35
+ PER_UNIT = "PER_UNIT"
36
+ FLAT_FEE = "FLAT_FEE"
37
+ PER_SEAT = "PER_SEAT"
38
+ TIERED = "TIERED"
39
+ VOLUME = "VOLUME"
40
+ end
41
+
42
+ module EntitlementPeriod
43
+ MONTHLY = "MONTHLY"
44
+ YEARLY = "YEARLY"
45
+ QUARTERLY = "QUARTERLY"
46
+ WEEKLY = "WEEKLY"
47
+ DAILY = "DAILY"
48
+ ONE_TIME = "ONE_TIME"
49
+ end
50
+
51
+ module CarryoverPolicy
52
+ EXPIRE_AT_PERIOD_END = "EXPIRE_AT_PERIOD_END"
53
+ ROLLOVER = "ROLLOVER"
54
+ ROLLOVER_CAPPED = "ROLLOVER_CAPPED"
55
+ end
56
+
57
+ module CostAmortization
58
+ PRORATA = "PRORATA"
59
+ MONTHS = "MONTHS"
60
+ QUARTER = "QUARTER"
61
+ YEARS = "YEARS"
62
+ end
63
+
64
+ MetricTupleHint = Struct.new(:app_name, :license_name, :metric_name, keyword_init: true) do
65
+ def to_h
66
+ { app_name: app_name, license_name: license_name, metric_name: metric_name }.compact
67
+ end
68
+ end
69
+
70
+ TrackEvent = Struct.new(
71
+ :metric, :quantity, :license_id, :unit, :kind,
72
+ :timestamp, :source_system, :metric_tuple_hint,
73
+ :attribution, :metadata,
74
+ keyword_init: true
75
+ ) do
76
+ def to_h
77
+ {
78
+ metric: metric,
79
+ quantity: quantity,
80
+ license_id: license_id,
81
+ unit: unit,
82
+ kind: kind || EventKind::USAGE,
83
+ timestamp: timestamp&.iso8601 || Time.now.utc.iso8601,
84
+ source_system: source_system,
85
+ metric_tuple_hint: metric_tuple_hint&.to_h,
86
+ attribution: attribution,
87
+ metadata: metadata
88
+ }.compact
89
+ end
90
+ end
91
+
92
+ class App
93
+ attr_accessor :id, :name, :description, :logo_url, :website, :categories, :saas_application_id, :created_at, :updated_at
94
+
95
+ def initialize(**attrs)
96
+ attrs.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
97
+ end
98
+ end
99
+
100
+ class License
101
+ attr_accessor :id, :name, :license_type, :contract_id, :seats, :price_per_seat, :total_cost,
102
+ :discount, :discount_amount, :discounted_cost, :original_cost, :tenure,
103
+ :seats_included_in_base_plan, :created_at, :updated_at
104
+
105
+ def initialize(**attrs)
106
+ attrs.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
107
+ end
108
+ end
109
+
110
+ class Expense
111
+ attr_accessor :id, :app_id, :contract_id, :license_id, :total, :date, :month, :year,
112
+ :description, :transaction_id, :vendor, :payment_channel, :currency,
113
+ :created_at, :updated_at
114
+
115
+ def initialize(**attrs)
116
+ @currency = "USD"
117
+ attrs.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
118
+ end
119
+ end
120
+
121
+ class Contract
122
+ attr_accessor :id, :app_id, :title, :contract_type, :start_date, :end_date, :currency,
123
+ :cost, :cost_break_down, :cost_amortization, :add_ons, :add_ons_total_cost,
124
+ :total_contract_cost, :total_discount_amount, :total_original_cost,
125
+ :licenses, :expenses, :created_at, :updated_at
126
+
127
+ def initialize(**attrs)
128
+ @licenses = []
129
+ @expenses = []
130
+ attrs.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
131
+ end
132
+ end
133
+
134
+ class Metric
135
+ attr_accessor :id, :license_id, :metric_type, :usage_aggregation_type, :rate_kind,
136
+ :entitlement_period, :carryover_policy, :usage_rate, :usage_limit,
137
+ :usage_included, :per_unit_cap, :usage_rate_is_estimated,
138
+ :usage_custom_unit_label, :expected_emission_interval_minutes,
139
+ :created_at, :updated_at
140
+
141
+ def initialize(**attrs)
142
+ attrs.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
143
+ end
144
+ end
145
+
146
+ class PaginatedResponse
147
+ attr_accessor :data, :cursor, :has_more
148
+
149
+ def initialize(**attrs)
150
+ attrs.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoowTrack
4
+ VERSION = "0.1.0"
5
+ end
data/lib/doow_track.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "doow_track/version"
4
+ require_relative "doow_track/error"
5
+ require_relative "doow_track/types"
6
+ require_relative "doow_track/tracker"
7
+ require_relative "doow_track/management"
8
+
9
+ module DoowTrack
10
+ class << self
11
+ attr_accessor :api_key, :endpoint, :debug
12
+
13
+ def configure
14
+ yield self
15
+ end
16
+ end
17
+
18
+ self.endpoint = "https://api.doow.co"
19
+ self.debug = false
20
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: doow_track
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Doow
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-23 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.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-http
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.18'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.18'
69
+ description: Track usage events and manage apps, contracts, licenses, and metrics
70
+ with the Doow API
71
+ email:
72
+ - dev@doow.co
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - LICENSE
78
+ - README.md
79
+ - lib/doow_track.rb
80
+ - lib/doow_track/error.rb
81
+ - lib/doow_track/management.rb
82
+ - lib/doow_track/tracker.rb
83
+ - lib/doow_track/types.rb
84
+ - lib/doow_track/version.rb
85
+ homepage: https://github.com/Doow-Dev/doow-track-ruby
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/Doow-Dev/doow-track-ruby
90
+ source_code_uri: https://github.com/Doow-Dev/doow-track-ruby
91
+ changelog_uri: https://github.com/Doow-Dev/doow-track-ruby/blob/main/CHANGELOG.md
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.0.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.4.19
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Official Ruby SDK for Doow usage telemetry and management
111
+ test_files: []