Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
7438ca5
chore: add altertable-client-specs submodule at v0.8.0
albert20260301 Mar 6, 2026
a3b2c62
chore: sync community files from v0.8.0 specs
albert20260301 Mar 6, 2026
d637475
Merge branch 'main' into update/specs-v0.8.0
albert20260301 Mar 6, 2026
4a83183
feat: implement product analytics API client
albert20260301 Mar 6, 2026
dc25ad6
test: integrate Testcontainers for Docker-based mock server testing
albert20260301 Mar 6, 2026
70bbc88
fix: make track() distinct_id argument mandatory for stateless client
albert20260301 Mar 6, 2026
6cda60d
chore: add rubocop and update CI matrix
Mar 6, 2026
7cd70a3
ci: allow CI to run on update/specs-v0.8.0 branch
Mar 6, 2026
6d4cf43
test: increase timeout for slow mock container
Mar 6, 2026
6e661dd
ci: increase job timeout for slow native extensions and mock container
Mar 6, 2026
d9b5c92
test: use fixed version for mock-server container
Mar 6, 2026
4c965a8
test: add error logging to debug connection refusal
Mar 6, 2026
bfd7452
Merge branch 'main' into update/specs-v0.8.0
redox Mar 6, 2026
89e6336
test: ensure mock-server container uses correct port internally
Mar 6, 2026
cd3a43d
test: fix incorrect port mapping in spec_helper (15001 -> 15000)
Mar 6, 2026
2bfbc40
test: fix default mock port in spec (15001 -> 15000)
Mar 6, 2026
b8c951e
test: cleanup duplicate request_timeout in spec
Mar 6, 2026
bbc8b2a
test: correctly expose port 15000 in mock-server container
Mar 6, 2026
1e93a13
test: wait for mock-server health check before running tests
Mar 6, 2026
b7acd81
test: revert wait_for and add debug logging to spec_helper
Mar 6, 2026
b73ee95
test: use 127.0.0.1 instead of localhost in spec
Mar 6, 2026
a474e94
test: wait for mock-server log message instead of http check
Mar 6, 2026
c08d3ee
test: use host network mode for mock-server to fix connection refused
Mar 6, 2026
eac091d
test: add more debug logging for testcontainers
Mar 6, 2026
a0e90d9
test: disable with_network_mode(host) and wait_for to isolate issue
Mar 6, 2026
45fac40
test: switch back to port 15001 and restore mapped_port
Mar 6, 2026
7f66511
test: major cleanup of test infrastructure and debug logging
Mar 6, 2026
f5076ba
chore: add testcontainers and base64 development dependencies
Mar 6, 2026
1b16c16
test: add error logging to spec_helper to debug container access
Mar 6, 2026
d43ef81
test: use latest mock-server image to avoid potential pull access den…
Mar 6, 2026
2e1bcc8
test: switch from testcontainers to webmock for CI reliability
Mar 6, 2026
a4c9833
Delete .github/workflows/test.yml
redox Mar 6, 2026
ae08c57
test: update spec to use Altertable.init instead of non-existent .con…
Mar 6, 2026
fdaa1f5
ci: enable CI on update/specs-v0.8.0 branch
Mar 6, 2026
842a855
test: use CI mock server instead of webmock
Mar 6, 2026
7620437
test: fix incorrect init call in spec
Mar 6, 2026
4116292
test: fix syntax error in let and use valid mock api key
Mar 6, 2026
296fec1
fix: match method signature in top-level module with Client
Mar 6, 2026
22fcd6a
ci: expose port 15000 for Lakehouse server in sidecar container
Mar 6, 2026
3fa6fc7
test: fix incorrect port in altertable_spec (15001 -> 15000)
Mar 6, 2026
04552d7
test: use valid mock api key prefix (pk_test_)
Mar 6, 2026
30f7229
test: hardcode base_url in Altertable.init to ensure correct port
Mar 6, 2026
28e8aec
test: use Lakehouse port 15000 in Altertable.init
Mar 6, 2026
4dac1aa
test: use service alias 'altertable' instead of 127.0.0.1 in CI
Mar 6, 2026
f14e4b7
test: use 127.0.0.1 instead of 'altertable' for sidecar port access
Mar 6, 2026
8375410
test: use 'localhost' instead of '127.0.0.1' for sidecar port access
Mar 6, 2026
000e95d
test: use service alias 'altertable' and fix CI config to ensure netw…
Mar 6, 2026
bf0d87f
test: use 127.0.0.1 for sidecar port access
Mar 6, 2026
c059872
test: use Product Analytics port 15001 for Altertable.init in CI
Mar 6, 2026
d56ce7c
test: ensure request_timeout is set to avoid potential 401/404 racing…
Mar 6, 2026
fb137b7
test: use localhost for Product Analytics port 15001
Mar 6, 2026
859daf8
test: use 127.0.0.1 for Product Analytics port 15001
Mar 6, 2026
b991567
test: use Lakehouse port 15000 for track/identify/alias tests in CI
Mar 6, 2026
6296eaa
test: switch to webmock to bypass CI sidecar connection issues
Mar 6, 2026
77065df
Fix
redox Mar 6, 2026
211669d
useless
redox Mar 6, 2026
3d2e87d
useless
redox Mar 6, 2026
2b5e9e5
no need of 15000
redox Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
ports:
- 15001:15001
env:
ALTERTABLE_MOCK_API_KEY: test_pk_abc123
ALTERTABLE_MOCK_API_KEYS: test_pk_abc123
options: >-
--health-cmd "exit 0"
--health-interval 5s
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "specs"]
path = specs
url = https://github.com/altertable-ai/altertable-client-specs.git
18 changes: 18 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require:
- rubocop-performance
- rubocop-rspec

AllCops:
NewCops: enable
Exclude:
- 'vendor/**/*'
- 'spec/spec_helper.rb'

Layout/LineLength:
Max: 120

Style/Documentation:
Enabled: false

Style/FrozenStringLiteralComment:
Enabled: true
Empty file added CHANGELOG.md
Empty file.
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
source "https://rubygems.org"

gemspec

gem "rspec"
gem "testcontainers"
gem "base64"
109 changes: 109 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
PATH
remote: .
specs:
altertable-ruby (0.1.0)

GEM
remote: https://rubygems.org/
specs:
addressable (2.8.9)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
base64 (0.3.0)
bigdecimal (4.0.1)
diff-lcs (1.6.2)
docker-api (2.4.0)
excon (>= 0.64.0)
multi_json
excon (1.4.0)
logger
json (2.19.0)
json-schema (6.2.0)
addressable (~> 2.8)
bigdecimal (>= 3.1, < 5)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
mcp (0.8.0)
json-schema (>= 4.1)
multi_json (1.19.1)
parallel (1.27.0)
parser (3.3.10.2)
ast (~> 2.4.1)
racc
prism (1.9.0)
public_suffix (7.0.5)
racc (1.8.1)
rainbow (3.1.1)
rake (13.3.1)
regexp_parser (2.11.3)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
rubocop (1.85.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
mcp (~> 0.6)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-capybara (2.22.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rspec (2.31.0)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.29.1)
rubocop (~> 1.61)
ruby-progressbar (1.13.0)
testcontainers (0.2.0)
testcontainers-core (= 0.2.0)
testcontainers-core (0.2.0)
docker-api (~> 2.2)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)

PLATFORMS
arm64-darwin-25
ruby

DEPENDENCIES
altertable-ruby!
base64
rake (~> 13.0)
rspec
rubocop (~> 1.0)
rubocop-performance (~> 1.0)
rubocop-rspec (~> 2.0)
testcontainers

BUNDLED WITH
2.6.7
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Altertable Ruby SDK

Official Ruby SDK for Altertable Product Analytics.

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'altertable-ruby'
```

And then execute:

$ bundle install

## Usage

### Initialization

```ruby
require 'altertable'

Altertable.init('your_api_key', {
environment: 'production'
})
```

### Tracking Events

```ruby
Altertable.track('button_clicked', 'user_123', {
button_id: 'signup_btn',
page: 'home'
})
```

### Identifying Users

```ruby
Altertable.identify('user_123', {
email: 'user@example.com',
name: 'John Doe'
})
```

### Alias

```ruby
Altertable.alias('new_user_id', 'previous_anonymous_id')
```

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
7 changes: 7 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

task default: :spec
32 changes: 32 additions & 0 deletions altertable-ruby.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

Gem::Specification.new do |spec|
spec.name = "altertable-ruby"
spec.version = "0.1.0"
spec.authors = ["Altertable"]
spec.email = ["support@api.altertable.ai"]

spec.summary = "Altertable Product Analytics Ruby SDK"
spec.description = "Official Ruby client for Altertable Product Analytics"
spec.homepage = "https://github.com/altertable-ai/altertable-ruby"
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
spec.metadata["changelog_uri"] = "https://github.com/altertable-ai/altertable-ruby/blob/main/CHANGELOG.md"

spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rubocop", "~> 1.0"
spec.add_development_dependency "rubocop-performance", "~> 1.0"
spec.add_development_dependency "rubocop-rspec", "~> 2.0"
spec.add_development_dependency "testcontainers"
end
31 changes: 31 additions & 0 deletions lib/altertable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require_relative "altertable/version"
require_relative "altertable/errors"
require_relative "altertable/client"

module Altertable
class << self
def init(api_key, options = {})
@client = Client.new(api_key, options)
end

def track(event, user_id, properties = {})
client.track(event, user_id, properties)
end

def identify(user_id, traits = {})
client.identify(user_id, traits)
end

def alias(new_user_id, previous_id)
client.alias(new_user_id, previous_id)
end

def client
raise ConfigurationError, "Altertable client not initialized. Call Altertable.init(api_key) first." unless @client

@client
end
end
end
109 changes: 109 additions & 0 deletions lib/altertable/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

require "net/http"
require "json"
require "time"
require_relative "errors"

module Altertable
class Client
DEFAULT_BASE_URL = "https://api.altertable.ai"
DEFAULT_TIMEOUT = 5
DEFAULT_ENVIRONMENT = "production"

def initialize(api_key, options = {})
raise ConfigurationError, "API Key is required" if api_key.nil? || api_key.empty?

@api_key = api_key
@base_url = options[:base_url] || DEFAULT_BASE_URL
@environment = options[:environment] || DEFAULT_ENVIRONMENT
@timeout = options[:request_timeout] || DEFAULT_TIMEOUT
@release = options[:release]
@debug = options[:debug] || false
@on_error = options[:on_error]
end

def track(event, distinct_id, properties = {})
payload = {
timestamp: Time.now.utc.iso8601(3),
event: event,
environment: @environment,
distinct_id: distinct_id,
properties: {
"$lib": "altertable-ruby",
"$lib_version": Altertable::VERSION
}.merge(properties)
}
payload[:properties]["$release"] = @release if @release

post("/track", payload)
end

def identify(user_id, traits = {})
payload = {
timestamp: Time.now.utc.iso8601(3),
environment: @environment,
distinct_id: user_id,
traits: traits
}

post("/identify", payload)
end

def alias(new_user_id, previous_id)
payload = {
timestamp: Time.now.utc.iso8601(3),
environment: @environment,
distinct_id: previous_id,
new_user_id: new_user_id
}

post("/alias", payload)
end

private

def post(path, payload)
uri = URI("#{@base_url}#{path}")
req = Net::HTTP::Post.new(uri)
req["X-API-Key"] = @api_key
req["Content-Type"] = "application/json"
req.body = payload.to_json

begin
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: @timeout) do |http|
http.request(req)
end

handle_response(res)
rescue StandardError => e
handle_error(e)
end
end

def handle_response(res)
case res.code.to_i
when 200..299
JSON.parse(res.body) rescue {}
when 422
error_data = JSON.parse(res.body) rescue {}
raise ApiError.new("Unprocessable Entity: #{error_data["message"]}", res.code, error_data)
else
raise ApiError.new("HTTP Error: #{res.code}", res.code)
end
end

def handle_error(error)
wrapped_error = if error.is_a?(AltertableError)
error
elsif error.is_a?(Net::ReadTimeout) || error.is_a?(Net::OpenTimeout)
NetworkError.new("Timeout: #{error.message}", error)
else
AltertableError.new(error.message, error)
end

@on_error&.call(wrapped_error)
raise wrapped_error
end
end
end
Loading
Loading