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 +7 -0
- data/LICENSE +21 -0
- data/README.md +99 -0
- data/lib/clamp_analytics/errors.rb +22 -0
- data/lib/clamp_analytics/money.rb +20 -0
- data/lib/clamp_analytics/version.rb +7 -0
- data/lib/clamp_analytics.rb +139 -0
- metadata +98 -0
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,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: []
|