-
Notifications
You must be signed in to change notification settings - Fork 1
chore: update specs to v0.8.0 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7438ca5
a3b2c62
d637475
4a83183
dc25ad6
70bbc88
6cda60d
7cd70a3
6d4cf43
6e661dd
d9b5c92
4c965a8
bfd7452
89e6336
cd3a43d
2bfbc40
b8c951e
bbc8b2a
1e93a13
b7acd81
b73ee95
a474e94
c08d3ee
eac091d
a0e90d9
45fac40
7f66511
f5076ba
1b16c16
d43ef81
2e1bcc8
a4c9833
ae08c57
fdaa1f5
842a855
7620437
4116292
296fec1
22fcd6a
3fa6fc7
04552d7
30f7229
28e8aec
4dac1aa
f14e4b7
8375410
000e95d
bf0d87f
c059872
d56ce7c
fb137b7
859daf8
b991567
6296eaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| source "https://rubygems.org" | ||
|
|
||
| gemspec | ||
|
|
||
| gem "rspec" | ||
| gem "testcontainers" |
| 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). | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # frozen_string_literal: true | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. File to be renamed as well |
||
|
|
||
| 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 "webmock" | ||
| end | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # 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" | ||
|
|
||
| RESERVED_USER_IDS = %w[ | ||
| anonymous_id anonymous distinct_id distinctid false guest | ||
| id not_authenticated true undefined user_id user | ||
| visitor_id visitor | ||
| ].freeze | ||
|
|
||
| RESERVED_USER_IDS_CASE_SENSITIVE = ["[object Object]", "0", "NaN", "none", "None", "null"].freeze | ||
|
Comment on lines
+14
to
+20
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't do this, you can remove
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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 = {}) | ||
| validate_user_id!(distinct_id) | ||
|
|
||
| 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 = {}) | ||
| validate_user_id!(user_id) | ||
|
|
||
| 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) | ||
| validate_user_id!(new_user_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 validate_user_id!(user_id) | ||
| return if user_id.nil? | ||
|
|
||
| id_str = user_id.to_s | ||
| if RESERVED_USER_IDS.include?(id_str.downcase) || RESERVED_USER_IDS_CASE_SENSITIVE.include?(id_str) | ||
| raise ArgumentError, "Reserved User ID: #{user_id}" | ||
| end | ||
| end | ||
|
|
||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @albert20260301 We should support other HTTP clients - see https://github.com/altertable-ai/altertable-client-specs/blob/main/.agents/skills/build-http-sdk/SKILL.md |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Altertable | ||
| class AltertableError < StandardError | ||
| attr_reader :cause | ||
|
|
||
| def initialize(message, cause = nil) | ||
| super(message) | ||
| @cause = cause | ||
| end | ||
| end | ||
|
|
||
| class ConfigurationError < AltertableError; end | ||
|
|
||
| class ApiError < AltertableError | ||
| attr_reader :status, :details | ||
|
|
||
| def initialize(message, status, details = {}) | ||
| super(message) | ||
| @status = status | ||
| @details = details | ||
| end | ||
| end | ||
|
|
||
| class NetworkError < AltertableError; end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Altertable | ||
| VERSION = "0.1.0" | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gem name should only be "altertable"