clamp-analytics 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: ffb1d4e3b88c1a033a823f87ca944677956ba43bb9899f17904d4e9009e14998
4
+ data.tar.gz: '046796aafe3d755145de3304ee15d9ca4210a90d1aa3706b1578c07d94eb797e'
5
+ SHA512:
6
+ metadata.gz: 5deb502c2bfa8ec1007c11cfb93e736dfd933e216ea4d9bc3c6f580d0da7b5d9575f58ac54ea28ecec069f1adb61e9a59e258c4f7054dd239c230ebc823ad10b
7
+ data.tar.gz: 57be92f93a604ebea4f33c4ca526fd89bd57866fbee0ea9d705cf7bd6c3eafb613e5337b66f9de6651f5af29bd4330de37831f4703bc536f686b10a8b106c3fd
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Clamp
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,99 @@
1
+ # clamp-analytics (Ruby)
2
+
3
+ Server-side analytics SDK for [Clamp Analytics](https://clamp.sh) in Ruby.
4
+
5
+ Send tracked events from any Ruby app to Clamp. Works with Rails, Sinatra, Sidekiq workers, scheduled jobs, and anything else that runs Ruby 3.0+ and can make outbound HTTPS calls.
6
+
7
+ ## Install
8
+
9
+ Add to your `Gemfile`:
10
+
11
+ ```ruby
12
+ gem "clamp-analytics"
13
+ ```
14
+
15
+ Or:
16
+
17
+ ```bash
18
+ gem install clamp-analytics
19
+ ```
20
+
21
+ Ruby 3.0+ supported. The gem uses only the standard library (`net/http`, `json`, `time`, `uri`).
22
+
23
+ ## Quick start
24
+
25
+ ```ruby
26
+ require "clamp_analytics"
27
+
28
+ Clamp::Analytics.init(
29
+ project_id: "proj_xxx",
30
+ api_key: ENV.fetch("CLAMP_API_KEY")
31
+ )
32
+
33
+ # Simple event
34
+ Clamp::Analytics.track("signup", properties: { plan: "pro", method: "email" })
35
+
36
+ # Link a server event to a browser visitor
37
+ Clamp::Analytics.track(
38
+ "subscription_started",
39
+ properties: {
40
+ plan: "pro",
41
+ total: Clamp::Analytics::Money.new(29.00, "USD")
42
+ },
43
+ anonymous_id: "aid_xxx"
44
+ )
45
+ ```
46
+
47
+ Get a server API key at <https://clamp.sh/dashboard> (Settings → API Keys, format `sk_proj_...`). Read it from the environment; never commit it.
48
+
49
+ ## API
50
+
51
+ ### `Clamp::Analytics.init(project_id:, api_key:, endpoint: nil)`
52
+
53
+ Initializes the SDK. Call once at app boot (Rails initializer, Sinatra setup, Sidekiq server middleware). State is held at the module level behind a Mutex, so it's safe across threads.
54
+
55
+ `endpoint` is optional and overrides the default `https://api.clamp.sh`.
56
+
57
+ ### `Clamp::Analytics.track(name, properties: {}, anonymous_id: nil, timestamp: nil)`
58
+
59
+ Sends a server event. Returns `true` on success.
60
+
61
+ - **`name`**: event name string. Examples: `"signup"`, `"subscription_started"`.
62
+ - **`properties`**: optional hash. Values may be `String`, `Integer`, `Float`, `true`/`false`, or `Money`. Other types raise `ArgumentError`.
63
+ - **`anonymous_id`**: optional string. Links the server event to a browser visitor.
64
+ - **`timestamp`**: optional `Time` (non-UTC times are normalized to UTC) or ISO 8601 string. If omitted, uses `Time.now.utc`.
65
+
66
+ Raises `Clamp::Analytics::HTTPError` on a non-2xx response, `Clamp::Analytics::NotInitializedError` if `init` wasn't called.
67
+
68
+ ### `Clamp::Analytics::Money`
69
+
70
+ ```ruby
71
+ Clamp::Analytics::Money.new(29.00, "USD")
72
+ ```
73
+
74
+ A typed monetary value. `amount` is in major units (29.00, not 2900). `currency` is an ISO 4217 code (uppercase, three letters).
75
+
76
+ ## Framework integrations
77
+
78
+ Per-framework integration patterns (Rails initializer + concern, Sinatra helper, Sidekiq middleware) are documented at <https://clamp.sh/docs/sdk/ruby>.
79
+
80
+ ## Errors
81
+
82
+ The gem is synchronous and raises on failure. There are no automatic retries. If you want fire-and-forget behaviour, rescue around the call:
83
+
84
+ ```ruby
85
+ begin
86
+ Clamp::Analytics.track("subscription_started", properties: ...)
87
+ rescue Clamp::Analytics::Error => e
88
+ Rails.logger.error("clamp: #{e.message}")
89
+ end
90
+ ```
91
+
92
+ For high-throughput webhook handlers, defer the call to a Sidekiq job.
93
+
94
+ ## Links
95
+
96
+ - RubyGems: <https://rubygems.org/gems/clamp-analytics>
97
+ - Docs: <https://clamp.sh/docs/sdk/ruby>
98
+ - Source: <https://github.com/clamp-sh/analytics-ruby>
99
+ - Issues: <https://github.com/clamp-sh/analytics-ruby/issues>
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clamp
4
+ module Analytics
5
+ # Base class for all Clamp SDK errors.
6
+ class Error < StandardError; end
7
+
8
+ # Raised when track is called before init.
9
+ class NotInitializedError < Error; end
10
+
11
+ # Raised when the ingestion API returns a non-2xx response.
12
+ class HTTPError < Error
13
+ attr_reader :status_code, :body
14
+
15
+ def initialize(status_code, body)
16
+ @status_code = status_code
17
+ @body = body
18
+ super("clamp_analytics: #{status_code} #{body}")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clamp
4
+ module Analytics
5
+ # A typed monetary value attached to any event property.
6
+ #
7
+ # Clamp::Analytics.track('purchase',
8
+ # properties: {
9
+ # plan: 'pro',
10
+ # total: Clamp::Analytics::Money.new(29.00, 'USD'),
11
+ # tax: Clamp::Analytics::Money.new(4.35, 'USD')
12
+ # }
13
+ # )
14
+ Money = Struct.new(:amount, :currency) do
15
+ def to_wire
16
+ { amount: amount, currency: currency }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clamp
4
+ module Analytics
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "time"
6
+ require "uri"
7
+
8
+ require_relative "clamp_analytics/version"
9
+ require_relative "clamp_analytics/money"
10
+ require_relative "clamp_analytics/errors"
11
+
12
+ # Server-side analytics SDK for Clamp.
13
+ #
14
+ # require "clamp_analytics"
15
+ #
16
+ # Clamp::Analytics.init(
17
+ # project_id: "proj_xxx",
18
+ # api_key: ENV.fetch("CLAMP_API_KEY")
19
+ # )
20
+ #
21
+ # Clamp::Analytics.track("signup", properties: { plan: "pro", method: "email" })
22
+ module Clamp
23
+ module Analytics
24
+ DEFAULT_ENDPOINT = "https://api.clamp.sh"
25
+
26
+ @config = nil
27
+ @transport = nil
28
+ @mutex = Mutex.new
29
+
30
+ class << self
31
+ # Initialize the SDK. Call once at application boot (Rails initializer,
32
+ # Sinatra setup block, etc.).
33
+ def init(project_id:, api_key:, endpoint: nil)
34
+ @mutex.synchronize do
35
+ @config = {
36
+ project_id: project_id,
37
+ api_key: api_key,
38
+ endpoint: endpoint || DEFAULT_ENDPOINT
39
+ }
40
+ end
41
+ end
42
+
43
+ # Track a server-side event.
44
+ #
45
+ # @param name [String] event name
46
+ # @param properties [Hash] optional, values may be String, Integer,
47
+ # Float, true/false, or Money
48
+ # @param anonymous_id [String, nil] optional, links to a browser visitor
49
+ # @param timestamp [Time, String, nil] optional; if omitted, uses Time.now.utc
50
+ # @raise [NotInitializedError] if init has not been called
51
+ # @raise [HTTPError] on non-2xx response
52
+ # @return [true]
53
+ def track(name, properties: {}, anonymous_id: nil, timestamp: nil)
54
+ cfg = @mutex.synchronize { @config }
55
+ raise NotInitializedError, "clamp_analytics: call Clamp::Analytics.init before track" if cfg.nil?
56
+
57
+ payload = { p: cfg[:project_id], name: name }
58
+ payload[:anonymousId] = anonymous_id unless anonymous_id.nil?
59
+ payload[:properties] = serialize_properties(properties) unless properties.empty?
60
+ payload[:timestamp] = serialize_timestamp(timestamp)
61
+
62
+ response = transport.call(
63
+ "#{cfg[:endpoint]}/e/s",
64
+ { "content-type" => "application/json", "x-clamp-key" => cfg[:api_key] },
65
+ JSON.generate(payload)
66
+ )
67
+
68
+ status = response[:status]
69
+ if status < 200 || status >= 300
70
+ raise HTTPError.new(status, response[:body].to_s)
71
+ end
72
+
73
+ true
74
+ end
75
+
76
+ # Override the transport. Used by tests; pass nil to restore the default.
77
+ def transport=(transport)
78
+ @mutex.synchronize { @transport = transport }
79
+ end
80
+
81
+ # Reset all SDK state. Intended for tests.
82
+ def reset!
83
+ @mutex.synchronize do
84
+ @config = nil
85
+ @transport = nil
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def transport
92
+ @transport || method(:default_transport)
93
+ end
94
+
95
+ def default_transport(url, headers, body)
96
+ uri = URI.parse(url)
97
+ http = Net::HTTP.new(uri.host, uri.port)
98
+ http.use_ssl = (uri.scheme == "https")
99
+ http.open_timeout = 5
100
+ http.read_timeout = 10
101
+
102
+ request = Net::HTTP::Post.new(uri.request_uri)
103
+ headers.each { |k, v| request[k] = v }
104
+ request.body = body
105
+
106
+ response = http.request(request)
107
+ { status: response.code.to_i, body: response.body || "" }
108
+ end
109
+
110
+ def serialize_properties(properties)
111
+ properties.each_with_object({}) do |(key, value), out|
112
+ out[key] = case value
113
+ when Money
114
+ value.to_wire
115
+ when String, Integer, Float, TrueClass, FalseClass
116
+ value
117
+ else
118
+ raise ArgumentError,
119
+ "clamp_analytics: property '#{key}' has unsupported type #{value.class}. " \
120
+ "Allowed: String, Integer, Float, true/false, Money."
121
+ end
122
+ end
123
+ end
124
+
125
+ def serialize_timestamp(timestamp)
126
+ case timestamp
127
+ when nil
128
+ Time.now.utc.iso8601
129
+ when Time
130
+ timestamp.utc.iso8601
131
+ when String
132
+ timestamp
133
+ else
134
+ raise ArgumentError, "clamp_analytics: timestamp must be Time or String, got #{timestamp.class}"
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clamp-analytics
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Clamp Analytics
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.60'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.60'
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.20'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.20'
55
+ description: Send tracked events from Ruby apps (Rails, Sinatra, Sidekiq, etc.) to
56
+ Clamp Analytics.
57
+ email:
58
+ - sidney@mail.clamp.sh
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE
64
+ - README.md
65
+ - lib/clamp_analytics.rb
66
+ - lib/clamp_analytics/errors.rb
67
+ - lib/clamp_analytics/money.rb
68
+ - lib/clamp_analytics/version.rb
69
+ homepage: https://clamp.sh
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ homepage_uri: https://clamp.sh
74
+ source_code_uri: https://github.com/clamp-sh/analytics-ruby
75
+ documentation_uri: https://clamp.sh/docs/sdk/ruby
76
+ bug_tracker_uri: https://github.com/clamp-sh/analytics-ruby/issues
77
+ changelog_uri: https://github.com/clamp-sh/analytics-ruby/blob/main/CHANGELOG.md
78
+ rubygems_mfa_required: 'true'
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.0.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.5.22
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Server-side analytics SDK for Clamp.
98
+ test_files: []