altertable 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7472be3a607d8398965c62ed90f5531fe33b21d77fa4827bd73259d7a714eba5
4
- data.tar.gz: 536319ad37e423c82ab298d5804aaf22de1563ece73226abaa42b5fd6bd93c39
3
+ metadata.gz: e63f277260656f5e0ed3c5696ae4fc974f99ddbbc75a6e983917d60d1b7af644
4
+ data.tar.gz: d52d275511b219aa0df3e8d9ee42e30830f3fb9fa399fe91a8198e35c15bbbc3
5
5
  SHA512:
6
- metadata.gz: 231d0b578dd8fe4ed8856d07fe233b4e256ce3650ce9a11260a21c01d352fcb9bc291477ab526c2d8984078caa4b879f92da667483af5e475d6ea08089999283
7
- data.tar.gz: 7ba2dd340e96f6d906c44456032b6a959e0cfa3f106bf5bf42d2982ac710da4856e4fb1e6d028a030afab62a3ed702e5a27e9fbd920725ff3b4926da40e64e35
6
+ metadata.gz: '0295a9615766d0fa8487568349a705705211cfb511087c0e79de3a6ac1804b3d33280e24167a7511dee768edb9f27f9c3fc83107af303af44ac62f5b519f8802'
7
+ data.tar.gz: de4d98a9ef278872d585e56941754d0a7b7515eb91065a2545f6e5e403b2d3eb5412c65cce24617426458aee040e03b4cd54f1775d4b28ac33f285cc311a236b
@@ -0,0 +1 @@
1
+ github: [altertable-ai]
@@ -0,0 +1,13 @@
1
+ ## What
2
+
3
+ <!-- What does this PR do? One sentence is fine. -->
4
+
5
+ ## Why
6
+
7
+ <!-- Link an issue or explain the motivation. -->
8
+
9
+ ## Checklist
10
+
11
+ - [ ] Tests added or updated
12
+ - [ ] `CHANGELOG.md` updated (skip for docs/CI-only changes)
13
+ - [ ] Breaking change noted in commit footer (`BREAKING CHANGE: …`) and changelog
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.2.0"
2
+ ".": "1.3.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/altertable-ai/altertable-ruby/compare/altertable/v1.2.0...altertable/v1.3.0) (2026-09-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * **client:** add batch track, identify, and alias ([#53](https://github.com/altertable-ai/altertable-ruby/issues/53)) ([2aff25b](https://github.com/altertable-ai/altertable-ruby/commit/2aff25b8b6f8767da113fce38901160dffbd9733))
9
+
3
10
  ## [1.2.0](https://github.com/altertable-ai/altertable-ruby/compare/altertable/v1.1.2...altertable/v1.2.0) (2026-08-27)
4
11
 
5
12
 
data/CODE_OF_CONDUCT.md CHANGED
@@ -70,14 +70,14 @@ Community leaders will follow these Community Impact Guidelines in determining t
70
70
 
71
71
  ## Attribution
72
72
 
73
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
73
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct/][v2.1].
74
74
 
75
75
  Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
76
76
 
77
- For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
77
+ For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq/][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations/][translations].
78
78
 
79
79
  [homepage]: https://www.contributor-covenant.org
80
- [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
81
- [Mozilla CoC]: https://github.com/mozilla/diversity
82
- [FAQ]: https://www.contributor-covenant.org/faq
83
- [translations]: https://www.contributor-covenant.org/translations
80
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/
81
+ [Mozilla CoC]: https://github.com/mozilla/inclusion
82
+ [FAQ]: https://www.contributor-covenant.org/faq/
83
+ [translations]: https://www.contributor-covenant.org/translations/
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- altertable (1.2.0)
4
+ altertable (1.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -47,38 +47,67 @@ Altertable.init('pk_live_abc123', environment: 'production', debug: true)
47
47
 
48
48
  Record an action performed by a user.
49
49
 
50
- `Altertable.track(event, distinct_id, **options)`
50
+ `Altertable.track(event, distinct_id, **options)`
51
+ `Altertable.track(event_payload, **options)`
52
+ `Altertable.track(event_payloads, **options)`
51
53
 
52
54
  ```ruby
53
55
  Altertable.track('item_purchased', 'user_123', properties: {
54
56
  item_id: 'item_999',
55
57
  price: 19.99
56
58
  })
59
+
60
+ Altertable.track({
61
+ event: 'item_purchased',
62
+ distinct_id: 'user_123',
63
+ properties: { item_id: 'item_999', price: 19.99 }
64
+ })
65
+
66
+ Altertable.track([
67
+ { event: 'signup', distinct_id: 'user_1', properties: { plan: 'pro' } },
68
+ { event: 'login', distinct_id: 'user_2', timestamp: '2025-06-15T14:30:00.000Z' }
69
+ ])
57
70
  ```
58
71
 
59
72
  ### Identifying Users
60
73
 
61
74
  Link a user ID to their traits (like email or name).
62
75
 
63
- `Altertable.identify(user_id, **options)`
76
+ `Altertable.identify(user_id, **options)`
77
+ `Altertable.identify(identify_payload, **options)`
78
+ `Altertable.identify(identify_payloads, **options)`
64
79
 
65
80
  ```ruby
66
81
  Altertable.identify('user_123', traits: {
67
82
  email: 'user@example.com',
68
83
  name: 'John Doe'
69
84
  })
85
+
86
+ Altertable.identify([
87
+ { user_id: 'user_1', traits: { email: 'one@example.com' } },
88
+ { user_id: 'user_2', traits: { email: 'two@example.com' } }
89
+ ])
70
90
  ```
71
91
 
72
92
  ### Alias
73
93
 
74
94
  Merge a previous anonymous ID with a newly identified user ID.
75
95
 
76
- `Altertable.alias(distinct_id, new_user_id, **options)`
96
+ `Altertable.alias(distinct_id, new_user_id, **options)`
97
+ `Altertable.alias(alias_payload, **options)`
98
+ `Altertable.alias(alias_payloads, **options)`
77
99
 
78
100
  ```ruby
79
101
  Altertable.alias('anon_session_456', 'user_123')
102
+
103
+ Altertable.alias([
104
+ { distinct_id: 'anon_1', new_user_id: 'user_1' },
105
+ { distinct_id: 'anon_2', new_user_id: 'user_2' }
106
+ ])
80
107
  ```
81
108
 
109
+ A Hash payload posts one object; an Array posts a JSON array on the same endpoint. Omitted `timestamp` values default to the current time as ISO 8601.
110
+
82
111
  ## Configuration
83
112
 
84
113
  You can configure the client by passing options during initialization.
data/SECURITY.md CHANGED
@@ -8,7 +8,7 @@ Only the latest minor release receives security patches.
8
8
 
9
9
  **Do not open a public issue.**
10
10
 
11
- Email security@altertable.ai with:
11
+ Email <security@altertable.ai> with:
12
12
 
13
13
  1. Description of the vulnerability
14
14
  2. Steps to reproduce
@@ -31,51 +31,40 @@ module Altertable
31
31
  @adapter = select_adapter(adapter_name, { base_url: @base_url, timeout: @timeout, headers: headers, proxy: options[:proxy] })
32
32
  end
33
33
 
34
- def track(event, distinct_id, **options)
35
- properties = options[:properties] || {}
36
- timestamp = options[:timestamp] || Time.now.utc.iso8601(3)
37
- payload = {
38
- timestamp: timestamp,
39
- event: event,
40
- environment: @environment,
41
- distinct_id: distinct_id,
42
- properties: {
43
- '$lib': "altertable-ruby",
44
- '$lib_version': Altertable::VERSION
45
- }.merge(properties)
46
- }
47
- payload[:properties]["$release"] = @release if @release
48
- payload[:anonymous_id] = options[:anonymous_id] if options.key?(:anonymous_id)
49
- payload[:device_id] = options[:device_id] if options.key?(:device_id)
50
-
51
- post("/track", payload)
34
+ def track(event, distinct_id = nil, **options)
35
+ case event
36
+ when Array
37
+ payloads = map_batch(event, "events") { |item| track_payload_from_item(merge_payload(item, options)) }
38
+ post("/track", payloads)
39
+ when Hash
40
+ post("/track", track_payload_from_item(merge_payload(event, options)))
41
+ else
42
+ post("/track", track_payload(event, distinct_id, options))
43
+ end
52
44
  end
53
45
 
54
46
  def identify(user_id, **options)
55
- traits = options[:traits] || {}
56
- timestamp = options[:timestamp] || Time.now.utc.iso8601(3)
57
- payload = {
58
- timestamp: timestamp,
59
- environment: @environment,
60
- distinct_id: user_id,
61
- traits: traits
62
- }
63
- payload[:anonymous_id] = options[:anonymous_id] if options.key?(:anonymous_id)
64
- payload[:device_id] = options[:device_id] if options.key?(:device_id)
65
-
66
- post("/identify", payload)
47
+ case user_id
48
+ when Array
49
+ payloads = map_batch(user_id, "identifies") { |item| identify_payload_from_item(merge_payload(item, options)) }
50
+ post("/identify", payloads)
51
+ when Hash
52
+ post("/identify", identify_payload_from_item(merge_payload(user_id, options)))
53
+ else
54
+ post("/identify", identify_payload(user_id, options))
55
+ end
67
56
  end
68
57
 
69
- def alias(distinct_id, new_user_id, **options)
70
- timestamp = options[:timestamp] || Time.now.utc.iso8601(3)
71
- payload = {
72
- timestamp: timestamp,
73
- environment: @environment,
74
- distinct_id: distinct_id,
75
- new_user_id: new_user_id
76
- }
77
-
78
- post("/alias", payload)
58
+ def alias(distinct_id, new_user_id = nil, **options)
59
+ case distinct_id
60
+ when Array
61
+ payloads = map_batch(distinct_id, "aliases") { |item| alias_payload_from_item(merge_payload(item, options)) }
62
+ post("/alias", payloads)
63
+ when Hash
64
+ post("/alias", alias_payload_from_item(merge_payload(distinct_id, options)))
65
+ else
66
+ post("/alias", alias_payload(distinct_id, new_user_id, options))
67
+ end
79
68
  end
80
69
 
81
70
  private
@@ -107,6 +96,108 @@ module Altertable
107
96
  false
108
97
  end
109
98
 
99
+ def map_batch(items, name, &block)
100
+ raise ArgumentError, "#{name} must be a non-empty Array" unless items.is_a?(Array) && !items.empty?
101
+
102
+ items.each_with_index do |item, index|
103
+ raise ArgumentError, "#{name}[#{index}] must be a Hash" unless item.is_a?(Hash)
104
+ end
105
+
106
+ items.map(&block)
107
+ end
108
+
109
+ def merge_payload(item, options)
110
+ return item if options.empty?
111
+
112
+ item.merge(options)
113
+ end
114
+
115
+ def item_value(item, key)
116
+ if item.key?(key)
117
+ item[key]
118
+ elsif item.key?(key.to_s)
119
+ item[key.to_s]
120
+ end
121
+ end
122
+
123
+ def item_options(item, *keys)
124
+ keys.each_with_object({}) do |key, opts|
125
+ opts[key] = item_value(item, key) if item.key?(key) || item.key?(key.to_s)
126
+ end
127
+ end
128
+
129
+ def blank?(value)
130
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
131
+ end
132
+
133
+ def default_timestamp
134
+ Time.now.utc.iso8601(3)
135
+ end
136
+
137
+ def track_payload_from_item(item)
138
+ event = item_value(item, :event)
139
+ distinct_id = item_value(item, :distinct_id)
140
+ raise ArgumentError, "event is required" if blank?(event)
141
+ raise ArgumentError, "distinct_id is required" if blank?(distinct_id)
142
+
143
+ track_payload(event, distinct_id, item_options(item, :properties, :anonymous_id, :device_id, :timestamp))
144
+ end
145
+
146
+ def identify_payload_from_item(item)
147
+ user_id = item_value(item, :user_id)
148
+ raise ArgumentError, "user_id is required" if blank?(user_id)
149
+
150
+ identify_payload(user_id, item_options(item, :traits, :anonymous_id, :device_id, :timestamp))
151
+ end
152
+
153
+ def alias_payload_from_item(item)
154
+ distinct_id = item_value(item, :distinct_id)
155
+ new_user_id = item_value(item, :new_user_id)
156
+ raise ArgumentError, "distinct_id is required" if blank?(distinct_id)
157
+ raise ArgumentError, "new_user_id is required" if blank?(new_user_id)
158
+
159
+ alias_payload(distinct_id, new_user_id, item_options(item, :timestamp))
160
+ end
161
+
162
+ def track_payload(event, distinct_id, options)
163
+ properties = options[:properties] || {}
164
+ payload = {
165
+ timestamp: options[:timestamp] || default_timestamp,
166
+ event: event,
167
+ environment: @environment,
168
+ distinct_id: distinct_id,
169
+ properties: {
170
+ '$lib': "altertable-ruby",
171
+ '$lib_version': Altertable::VERSION
172
+ }.merge(properties)
173
+ }
174
+ payload[:properties]["$release"] = @release if @release
175
+ payload[:anonymous_id] = options[:anonymous_id] if options.key?(:anonymous_id)
176
+ payload[:device_id] = options[:device_id] if options.key?(:device_id)
177
+ payload
178
+ end
179
+
180
+ def identify_payload(user_id, options)
181
+ payload = {
182
+ timestamp: options[:timestamp] || default_timestamp,
183
+ environment: @environment,
184
+ distinct_id: user_id,
185
+ traits: options[:traits] || {}
186
+ }
187
+ payload[:anonymous_id] = options[:anonymous_id] if options.key?(:anonymous_id)
188
+ payload[:device_id] = options[:device_id] if options.key?(:device_id)
189
+ payload
190
+ end
191
+
192
+ def alias_payload(distinct_id, new_user_id, options)
193
+ {
194
+ timestamp: options[:timestamp] || default_timestamp,
195
+ environment: @environment,
196
+ distinct_id: distinct_id,
197
+ new_user_id: new_user_id
198
+ }
199
+ end
200
+
110
201
  def post(path, payload)
111
202
  res = @adapter.post(path, body: payload.to_json)
112
203
  handle_response(res)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Altertable
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/altertable.rb CHANGED
@@ -10,7 +10,7 @@ module Altertable
10
10
  @client = Client.new(api_key, options)
11
11
  end
12
12
 
13
- def track(event, distinct_id, **options)
13
+ def track(event, distinct_id = nil, **options)
14
14
  client.track(event, distinct_id, **options)
15
15
  end
16
16
 
@@ -18,7 +18,7 @@ module Altertable
18
18
  client.identify(user_id, **options)
19
19
  end
20
20
 
21
- def alias(distinct_id, new_user_id, **options)
21
+ def alias(distinct_id, new_user_id = nil, **options)
22
22
  client.alias(distinct_id, new_user_id, **options)
23
23
  end
24
24
 
data/rbi/altertable.rbi CHANGED
@@ -34,14 +34,14 @@ module Altertable
34
34
  sig { params(api_key: String, options: T::Hash[Symbol, T.untyped]).void }
35
35
  def initialize(api_key, options = {}); end
36
36
 
37
- sig { params(event: String, distinct_id: String, options: T.untyped).returns(T.untyped) }
38
- def track(event, distinct_id, **options); end
37
+ sig { params(event: T.any(String, T::Hash[T.any(Symbol, String), T.untyped], T::Array[T::Hash[T.any(Symbol, String), T.untyped]]), distinct_id: T.nilable(String), options: T.untyped).returns(T.untyped) }
38
+ def track(event, distinct_id = nil, **options); end
39
39
 
40
- sig { params(user_id: String, options: T.untyped).returns(T.untyped) }
40
+ sig { params(user_id: T.any(String, T::Hash[T.any(Symbol, String), T.untyped], T::Array[T::Hash[T.any(Symbol, String), T.untyped]]), options: T.untyped).returns(T.untyped) }
41
41
  def identify(user_id, **options); end
42
42
 
43
- sig { params(distinct_id: String, new_user_id: String, options: T.untyped).returns(T.untyped) }
44
- def alias(distinct_id, new_user_id, **options); end
43
+ sig { params(distinct_id: T.any(String, T::Hash[T.any(Symbol, String), T.untyped], T::Array[T::Hash[T.any(Symbol, String), T.untyped]]), new_user_id: T.nilable(String), options: T.untyped).returns(T.untyped) }
44
+ def alias(distinct_id, new_user_id = nil, **options); end
45
45
 
46
46
  private
47
47
 
@@ -51,7 +51,7 @@ module Altertable
51
51
  sig { params(gem_name: String).returns(T::Boolean) }
52
52
  def try_require(gem_name); end
53
53
 
54
- sig { params(path: String, payload: T::Hash[T.any(Symbol, String), T.untyped]).returns(T.untyped) }
54
+ sig { params(path: String, payload: T.any(T::Hash[T.any(Symbol, String), T.untyped], T::Array[T::Hash[T.any(Symbol, String), T.untyped]])).returns(T.untyped) }
55
55
  def post(path, payload); end
56
56
 
57
57
  sig { params(res: T.untyped).returns(T.untyped) }
@@ -124,14 +124,14 @@ module Altertable
124
124
  sig { params(api_key: String, options: T::Hash[Symbol, T.untyped]).returns(Client) }
125
125
  def self.init(api_key, options = {}); end
126
126
 
127
- sig { params(event: String, distinct_id: String, options: T.untyped).returns(T.untyped) }
128
- def self.track(event, distinct_id, **options); end
127
+ sig { params(event: T.any(String, T::Hash[T.any(Symbol, String), T.untyped], T::Array[T::Hash[T.any(Symbol, String), T.untyped]]), distinct_id: T.nilable(String), options: T.untyped).returns(T.untyped) }
128
+ def self.track(event, distinct_id = nil, **options); end
129
129
 
130
- sig { params(user_id: String, options: T.untyped).returns(T.untyped) }
130
+ sig { params(user_id: T.any(String, T::Hash[T.any(Symbol, String), T.untyped], T::Array[T::Hash[T.any(Symbol, String), T.untyped]]), options: T.untyped).returns(T.untyped) }
131
131
  def self.identify(user_id, **options); end
132
132
 
133
- sig { params(distinct_id: String, new_user_id: String, options: T.untyped).returns(T.untyped) }
134
- def self.alias(distinct_id, new_user_id, **options); end
133
+ sig { params(distinct_id: T.any(String, T::Hash[T.any(Symbol, String), T.untyped], T::Array[T::Hash[T.any(Symbol, String), T.untyped]]), new_user_id: T.nilable(String), options: T.untyped).returns(T.untyped) }
134
+ def self.alias(distinct_id, new_user_id = nil, **options); end
135
135
 
136
136
  sig { returns(Client) }
137
137
  def self.client; end
@@ -16,16 +16,22 @@ module Altertable
16
16
  def initialize: (String api_key, ?::Hash[Symbol, untyped] options) -> void
17
17
 
18
18
  def track: (String event, String distinct_id, **untyped options) -> untyped
19
+ | (::Hash[Symbol | String, untyped] event_payload, **untyped options) -> untyped
20
+ | (::Array[::Hash[Symbol | String, untyped]] event_payloads, **untyped options) -> untyped
19
21
 
20
22
  def identify: (String user_id, **untyped options) -> untyped
23
+ | (::Hash[Symbol | String, untyped] identify_payload, **untyped options) -> untyped
24
+ | (::Array[::Hash[Symbol | String, untyped]] identify_payloads, **untyped options) -> untyped
21
25
 
22
26
  def alias: (String distinct_id, String new_user_id, **untyped options) -> untyped
27
+ | (::Hash[Symbol | String, untyped] alias_payload, **untyped options) -> untyped
28
+ | (::Array[::Hash[Symbol | String, untyped]] alias_payloads, **untyped options) -> untyped
23
29
 
24
30
  private
25
31
 
26
32
  def select_adapter: (Symbol? name, ::Hash[Symbol, untyped] options) -> untyped
27
33
  def try_require: (String gem_name) -> bool
28
- def post: (String path, ::Hash[Symbol | String, untyped] payload) -> untyped
34
+ def post: (String path, ::Hash[Symbol | String, untyped] | ::Array[::Hash[Symbol | String, untyped]] payload) -> untyped
29
35
  def handle_response: (untyped res) -> untyped
30
36
  def handle_error: (Exception error) -> untyped
31
37
  end
data/sig/altertable.rbs CHANGED
@@ -4,10 +4,16 @@ module Altertable
4
4
  def self.init: (String api_key, ?::Hash[Symbol, untyped] options) -> Client
5
5
 
6
6
  def self.track: (String event, String distinct_id, **untyped options) -> untyped
7
+ | (::Hash[Symbol | String, untyped] event_payload, **untyped options) -> untyped
8
+ | (::Array[::Hash[Symbol | String, untyped]] event_payloads, **untyped options) -> untyped
7
9
 
8
10
  def self.identify: (String user_id, **untyped options) -> untyped
11
+ | (::Hash[Symbol | String, untyped] identify_payload, **untyped options) -> untyped
12
+ | (::Array[::Hash[Symbol | String, untyped]] identify_payloads, **untyped options) -> untyped
9
13
 
10
14
  def self.alias: (String distinct_id, String new_user_id, **untyped options) -> untyped
15
+ | (::Hash[Symbol | String, untyped] alias_payload, **untyped options) -> untyped
16
+ | (::Array[::Hash[Symbol | String, untyped]] alias_payloads, **untyped options) -> untyped
11
17
 
12
18
  def self.client: () -> Client
13
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: altertable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Altertable
@@ -170,8 +170,10 @@ executables: []
170
170
  extensions: []
171
171
  extra_rdoc_files: []
172
172
  files:
173
+ - ".github/FUNDING.yml"
173
174
  - ".github/ISSUE_TEMPLATE/bug_report.yml"
174
175
  - ".github/ISSUE_TEMPLATE/feature_request.yml"
176
+ - ".github/PULL_REQUEST_TEMPLATE.md"
175
177
  - ".github/workflows/ci.yml"
176
178
  - ".github/workflows/release-please.yml"
177
179
  - ".github/workflows/semantic-pull-request.yml"