aptabase-ruby 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: a789fcfdf15e96d57c8f3003265c50ab4f495636db7bba375e8187772c560460
4
+ data.tar.gz: e8729f5338cba96e112237c68b09d135575f29f354b8e5d3072177cd61a5d393
5
+ SHA512:
6
+ metadata.gz: 6b797acc2b0b396ad410cf25f70daea999c2ad6bd37be07851b3df7e18b9ca62b3baebed4c903b800f3ea7b5657d09d30e2569be8cffa4ab0d90a98b172638af
7
+ data.tar.gz: 97c6b0dd5c9df7f186506ac1e6b5a76ad839abbd4a12d6579fb6fce08cc3a1af4ee2260d2e40b0ab976f702fb132cec5be3f224adef9e0396dafe35f16c822fd
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-06-11
8
+
9
+ Initial release, ported from [aptabase-python](https://github.com/aptabase/aptabase-python)
10
+ and verified against the live Aptabase ingestion API.
11
+
12
+ ### Added
13
+
14
+ - `Aptabase.init` / `Aptabase.track` / `Aptabase.flush` / `Aptabase.stop` module-level singleton API
15
+ - `Aptabase::Client` instance API, including a block form that guarantees flush on exit
16
+ - Background worker thread with auto-batching (max 25 events) and periodic flushing (default 10s)
17
+ - Failed batches are re-queued and retried; network errors never raise from `track`
18
+ - Session tracking with a 1-hour inactivity timeout, using the id format shared by all Aptabase SDKs
19
+ - Fork-safety: the worker thread restarts automatically in forked Puma/Unicorn workers
20
+ - Pending events are flushed automatically at process exit
21
+ - EU/US region routing derived from the app key; self-hosted instances via `base_url:`
22
+ - Rails integration: railtie that defaults the SDK logger to `Rails.logger`
23
+ - `rails generate aptabase:install` generator for the initializer
24
+ - Runnable samples: a plain Ruby script and a single-file Rails app
25
+ - CI across Ruby 3.1-3.4 and Rails 7.1, 7.2, 8.0 and latest
26
+
27
+ [0.1.0]: https://github.com/tiny-cloud-ventures/aptabase-ruby/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tiny Cloud Ventures
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,185 @@
1
+ # Aptabase Ruby SDK
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/aptabase-ruby.svg)](https://rubygems.org/gems/aptabase-ruby)
4
+ [![CI](https://github.com/tiny-cloud-ventures/aptabase-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/tiny-cloud-ventures/aptabase-ruby/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ Ruby and Rails SDK for [Aptabase](https://aptabase.com/) - open source, privacy-first
8
+ analytics for mobile, desktop and web applications.
9
+
10
+ > **Status:** 0.x - the API is stable and verified against the live Aptabase
11
+ > ingestion API, but minor releases may still include breaking changes until 1.0.
12
+
13
+ ## Features
14
+
15
+ - 🧵 **Non-blocking** - `track` queues in memory; a background thread does the sending
16
+ - 🔒 **Privacy-first** - no personal data collection, no device identifiers
17
+ - 🔄 **Auto-batching** - events are batched (max 25) and flushed every 10 seconds
18
+ - ♻️ **Resilient** - failed batches are re-queued and retried; tracking never raises on network errors
19
+ - ⚡ **Zero dependencies** - stdlib only
20
+ - 🛤️ **Rails-friendly** - install generator, `Rails.logger` integration, fork-safe under Puma cluster mode, flushes on exit
21
+
22
+ ## Requirements
23
+
24
+ - Ruby 3.1+
25
+ - Rails 7.1+ (optional - the SDK works in any Ruby program)
26
+
27
+ ## Installation
28
+
29
+ Add to your Gemfile:
30
+
31
+ ```ruby
32
+ gem "aptabase-ruby"
33
+ ```
34
+
35
+ Or install directly:
36
+
37
+ ```bash
38
+ gem install aptabase-ruby
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```ruby
44
+ require "aptabase"
45
+
46
+ Aptabase.init("A-EU-1234567890") # your app key
47
+
48
+ # Track a simple event
49
+ Aptabase.track("app_started")
50
+
51
+ # Track an event with properties
52
+ Aptabase.track("user_action", {
53
+ "action" => "button_click",
54
+ "button_id" => "login",
55
+ "screen" => "home"
56
+ })
57
+
58
+ # Events are flushed automatically (and on process exit),
59
+ # but you can force it:
60
+ Aptabase.flush
61
+ ```
62
+
63
+ ## Rails
64
+
65
+ Add the gem, then generate the initializer:
66
+
67
+ ```bash
68
+ rails generate aptabase:install
69
+ ```
70
+
71
+ This creates `config/initializers/aptabase.rb`, which reads the app key from
72
+ `Rails.application.credentials.dig(:aptabase, :app_key)` or `ENV["APTABASE_APP_KEY"]`.
73
+
74
+ Then call `Aptabase.track` anywhere - controllers, jobs, models:
75
+
76
+ ```ruby
77
+ class OrdersController < ApplicationController
78
+ def create
79
+ # ...
80
+ Aptabase.track("order_created", { "total_cents" => order.total_cents })
81
+ end
82
+ end
83
+ ```
84
+
85
+ The SDK logs through `Rails.logger` by default, restarts its background thread
86
+ automatically in forked Puma/Unicorn workers, and flushes pending events when
87
+ the process exits.
88
+
89
+ See [samples/rails-app](samples/rails-app) for a runnable single-file example.
90
+
91
+ ## Configuration
92
+
93
+ ```ruby
94
+ Aptabase.init(
95
+ "A-EU-1234567890", # Your Aptabase app key
96
+ app_version: "1.2.3", # Your app's version, shown in the dashboard
97
+ is_debug: false, # Debug events are kept separate in Aptabase
98
+ max_batch_size: 25, # Max events per request (hard max 25)
99
+ flush_interval: 10.0, # Auto-flush interval in seconds
100
+ timeout: 30.0, # HTTP timeout in seconds
101
+ logger: Logger.new($stderr) # Where SDK warnings/debug logs go
102
+ )
103
+ ```
104
+
105
+ ## App Key Format
106
+
107
+ Your app key determines the server region:
108
+
109
+ | Key prefix | Region |
110
+ | ---------- | ----------------------------------------------- |
111
+ | `A-EU-*` | European servers |
112
+ | `A-US-*` | US servers |
113
+ | `A-SH-*` | Self-hosted - requires the `base_url:` option |
114
+
115
+ Get your app key from the [Aptabase dashboard](https://aptabase.com/).
116
+
117
+ ```ruby
118
+ # Self-hosted instance
119
+ Aptabase.init("A-SH-1234567890", base_url: "https://analytics.example.com")
120
+ ```
121
+
122
+ ## Instance API
123
+
124
+ For multiple apps or explicit lifecycle control, use `Aptabase::Client` directly:
125
+
126
+ ```ruby
127
+ client = Aptabase::Client.new("A-EU-1234567890", app_version: "1.0.0")
128
+ client.track("event")
129
+ client.flush # synchronous send
130
+ client.pending_count # events queued, not yet delivered
131
+ client.stop # flushes remaining events
132
+
133
+ # Block form - stop/flush guaranteed on exit
134
+ Aptabase::Client.start("A-EU-1234567890") do |client|
135
+ client.track("event")
136
+ end
137
+ ```
138
+
139
+ ## Sessions
140
+
141
+ Events are grouped into sessions automatically. A session id is reused until
142
+ one hour of inactivity, then rotated - the same semantics and id format as the
143
+ official Aptabase SDKs, so dashboards behave identically.
144
+
145
+ ## Error Handling
146
+
147
+ Network failures never raise from `track` - failed batches are logged,
148
+ re-queued and retried on the next flush. Programmer errors do raise:
149
+
150
+ | Error | Raised when |
151
+ | ----------------------------- | -------------------------------------------- |
152
+ | `Aptabase::ConfigurationError`| bad app key or invalid options |
153
+ | `Aptabase::ValidationError` | bad event name or props |
154
+ | `Aptabase::NetworkError` | internal transport failures (`#status_code`) |
155
+
156
+ All inherit from `Aptabase::Error`.
157
+
158
+ ## Samples
159
+
160
+ - [Simple script](samples/simple-script) - plain Ruby, no framework
161
+ - [Rails app](samples/rails-app) - single-file Rails app via `bundler/inline`
162
+
163
+ ## Development
164
+
165
+ ```bash
166
+ bundle install
167
+ bundle exec rspec # run tests
168
+ bundle exec rubocop # lint
169
+
170
+ # run the suite against a specific Rails version
171
+ BUNDLE_GEMFILE=gemfiles/rails_80.gemfile bundle install
172
+ BUNDLE_GEMFILE=gemfiles/rails_80.gemfile bundle exec rspec
173
+ ```
174
+
175
+ ## Contributing
176
+
177
+ Bug reports and pull requests are welcome at
178
+ [tiny-cloud-ventures/aptabase-ruby](https://github.com/tiny-cloud-ventures/aptabase-ruby/issues).
179
+
180
+ ## License
181
+
182
+ [MIT](LICENSE)
183
+
184
+ This is a community SDK, ported from the official
185
+ [aptabase-python](https://github.com/aptabase/aptabase-python) SDK.
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Aptabase
6
+ # Aptabase analytics client.
7
+ #
8
+ # `track` is synchronous and cheap: it validates the event and pushes it
9
+ # onto an in-memory queue. A background worker thread flushes the queue
10
+ # every `flush_interval` seconds, or as soon as `max_batch_size` events
11
+ # are pending. Failed batches are re-queued and retried on the next flush.
12
+ class Client
13
+ HOSTS = {
14
+ "EU" => "https://eu.aptabase.com",
15
+ "US" => "https://us.aptabase.com"
16
+ }.freeze
17
+
18
+ MAX_BATCH_SIZE = 25
19
+
20
+ attr_reader :app_key, :base_url, :logger
21
+
22
+ # Block form: yields the client and guarantees stop/flush on exit.
23
+ #
24
+ # Aptabase::Client.start("A-EU-123") { |client| client.track("event") }
25
+ def self.start(app_key, **options)
26
+ client = new(app_key, **options)
27
+ return client unless block_given?
28
+
29
+ begin
30
+ yield client
31
+ ensure
32
+ client.stop
33
+ end
34
+ end
35
+
36
+ def initialize(app_key, app_version: "1.0.0", is_debug: false,
37
+ max_batch_size: MAX_BATCH_SIZE, flush_interval: 10.0,
38
+ timeout: 30.0, base_url: nil, logger: nil)
39
+ validate_app_key!(app_key)
40
+ raise ConfigurationError, "Maximum batch size is #{MAX_BATCH_SIZE} events" if max_batch_size > MAX_BATCH_SIZE
41
+ raise ConfigurationError, "max_batch_size must be at least 1" if max_batch_size < 1
42
+
43
+ @app_key = app_key
44
+ @base_url = base_url || default_base_url(app_key)
45
+ @system_props = SystemProperties.new(app_version: app_version, is_debug: is_debug)
46
+ @max_batch_size = max_batch_size
47
+ @flush_interval = flush_interval
48
+ @logger = logger || Logger.new($stderr, progname: "aptabase", level: Logger::INFO)
49
+ @transport = Transport.new(base_url: @base_url, app_key: app_key, timeout: timeout)
50
+ @session = Session.new
51
+
52
+ @queue = []
53
+ @queue_mutex = Mutex.new
54
+ @flush_mutex = Mutex.new
55
+ @wake = ConditionVariable.new
56
+ @worker = nil
57
+ @stopping = false
58
+ @pid = Process.pid
59
+ end
60
+
61
+ # Track an analytics event.
62
+ #
63
+ # client.track("app_started")
64
+ # client.track("purchase", { "product_id" => "abc", "price" => 29.99 })
65
+ def track(event_name, props = nil)
66
+ unless event_name.is_a?(String) && !event_name.empty?
67
+ raise ValidationError, "Event name is required and must be a non-empty string"
68
+ end
69
+ raise ValidationError, "Event properties must be a Hash" if !props.nil? && !props.is_a?(Hash)
70
+
71
+ event = Event.new(name: event_name, session_id: @session.id, props: props)
72
+
73
+ @queue_mutex.synchronize do
74
+ ensure_worker
75
+ @queue << event
76
+ @wake.signal if @queue.size >= @max_batch_size
77
+ end
78
+ nil
79
+ end
80
+
81
+ # Synchronously send all queued events in batches of `max_batch_size`.
82
+ # On failure the unsent events stay queued for the next flush.
83
+ def flush
84
+ @flush_mutex.synchronize do
85
+ loop do
86
+ batch = @queue_mutex.synchronize { @queue.shift(@max_batch_size) }
87
+ break if batch.empty?
88
+
89
+ begin
90
+ @transport.post_events(batch.map { |event| event.to_h(@system_props) })
91
+ logger.debug { "[aptabase] sent #{batch.size} event(s)" }
92
+ rescue NetworkError => e
93
+ logger.warn("[aptabase] failed to send #{batch.size} event(s), will retry: #{e.message}")
94
+ @queue_mutex.synchronize { @queue.unshift(*batch) }
95
+ break
96
+ end
97
+ end
98
+ end
99
+ nil
100
+ end
101
+
102
+ # Stop the background worker and flush any remaining events.
103
+ # The client can keep being used afterwards; tracking restarts the worker.
104
+ def stop
105
+ worker = nil
106
+ @queue_mutex.synchronize do
107
+ @stopping = true
108
+ worker = @worker
109
+ @wake.broadcast
110
+ end
111
+ worker.join(@flush_interval + 1) if worker && worker != Thread.current
112
+ flush
113
+ @queue_mutex.synchronize do
114
+ @worker = nil
115
+ @stopping = false
116
+ end
117
+ nil
118
+ end
119
+
120
+ # Number of events queued and not yet delivered.
121
+ def pending_count
122
+ @queue_mutex.synchronize { @queue.size }
123
+ end
124
+
125
+ private
126
+
127
+ def validate_app_key!(app_key)
128
+ unless app_key.is_a?(String) && !app_key.empty?
129
+ raise ConfigurationError, "App key is required and must be a string"
130
+ end
131
+
132
+ parts = app_key.split("-")
133
+ return if parts.length == 3 && parts[0] == "A" && (HOSTS.key?(parts[1]) || parts[1] == "SH")
134
+
135
+ raise ConfigurationError, "Invalid app key format. Expected format: A-{REGION}-{ID}"
136
+ end
137
+
138
+ def default_base_url(app_key)
139
+ region = app_key.split("-")[1]
140
+ raise ConfigurationError, "Self-hosted app keys (A-SH-*) require the base_url option" if region == "SH"
141
+
142
+ HOSTS.fetch(region)
143
+ end
144
+
145
+ # Must be called with @queue_mutex held.
146
+ def ensure_worker
147
+ if @pid != Process.pid
148
+ # Forked child: the parent's worker thread does not survive fork.
149
+ @pid = Process.pid
150
+ @worker = nil
151
+ @stopping = false
152
+ end
153
+ return if @stopping || @worker&.alive?
154
+
155
+ @worker = Thread.new { worker_loop }
156
+ @worker.name = "aptabase-flusher"
157
+ end
158
+
159
+ def worker_loop
160
+ loop do
161
+ stopping = false
162
+ @queue_mutex.synchronize do
163
+ @wake.wait(@queue_mutex, @flush_interval) unless @stopping || @queue.size >= @max_batch_size
164
+ stopping = @stopping
165
+ end
166
+ flush
167
+ break if stopping
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aptabase
4
+ # Base class for all errors raised by the SDK.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the SDK is misconfigured (bad app key, invalid options).
8
+ class ConfigurationError < Error; end
9
+
10
+ # Raised when event data fails validation.
11
+ class ValidationError < Error; end
12
+
13
+ # Raised when a request to the Aptabase API fails.
14
+ class NetworkError < Error
15
+ attr_reader :status_code
16
+
17
+ def initialize(message, status_code: nil)
18
+ super(message)
19
+ @status_code = status_code
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module Aptabase
7
+ # An analytics event queued for delivery to Aptabase.
8
+ class Event
9
+ attr_reader :name, :timestamp, :session_id, :props
10
+
11
+ def initialize(name:, timestamp: nil, session_id: nil, props: nil)
12
+ @name = name
13
+ @timestamp = (timestamp || Time.now).utc
14
+ @session_id = session_id || SecureRandom.uuid
15
+ @props = props || {}
16
+ end
17
+
18
+ def to_h(system_props)
19
+ {
20
+ "timestamp" => timestamp.iso8601(3),
21
+ "sessionId" => session_id,
22
+ "eventName" => name,
23
+ "systemProps" => system_props.to_h,
24
+ "props" => props
25
+ }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Aptabase
6
+ # Rails integration. Kept deliberately thin: configuration happens
7
+ # explicitly in an initializer (see `rails g aptabase:install`); the
8
+ # railtie only wires the SDK's default logger to Rails.logger.
9
+ #
10
+ # Process-exit flushing and Puma/Unicorn fork-safety are handled by the
11
+ # core SDK and need no Rails-specific hooks.
12
+ class Railtie < ::Rails::Railtie
13
+ # after_initialize, not an initializer hook: Rails may wrap or replace
14
+ # Rails.logger during boot (e.g. with BroadcastLogger), so capture the
15
+ # final object once the app is fully initialized.
16
+ config.after_initialize do
17
+ Aptabase.default_logger ||= ::Rails.logger
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aptabase
4
+ # Session tracking with the same semantics as the other Aptabase SDKs:
5
+ # a session id is reused until one hour of inactivity, then rotated.
6
+ class Session
7
+ TIMEOUT = 60 * 60 # seconds
8
+
9
+ def initialize(clock: -> { Time.now })
10
+ @clock = clock
11
+ @mutex = Mutex.new
12
+ @id = nil
13
+ @last_touched = nil
14
+ end
15
+
16
+ def id
17
+ @mutex.synchronize do
18
+ now = @clock.call
19
+ @id = generate_id(now) if @id.nil? || now - @last_touched > TIMEOUT
20
+ @last_touched = now
21
+ @id
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Same id format as the other Aptabase SDKs:
28
+ # epoch seconds * 1e8 plus an 8-digit random part.
29
+ def generate_id(now)
30
+ ((now.to_i * 100_000_000) + rand(100_000_000)).to_s
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module Aptabase
6
+ # System properties automatically collected by the SDK and attached to
7
+ # every event. Mirrors the fields sent by the other Aptabase SDKs.
8
+ class SystemProperties
9
+ attr_reader :locale, :os_name, :os_version, :device_model,
10
+ :is_debug, :app_version, :sdk_version
11
+
12
+ def initialize(app_version: "1.0.0", is_debug: false)
13
+ uname = Etc.uname
14
+ @locale = detect_locale
15
+ @os_name = uname[:sysname]
16
+ @os_version = uname[:release]
17
+ @device_model = uname[:machine]
18
+ @is_debug = is_debug
19
+ @app_version = app_version
20
+ @sdk_version = "aptabase-ruby@#{VERSION}"
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ "locale" => locale,
26
+ "osName" => os_name,
27
+ "osVersion" => os_version,
28
+ "deviceModel" => device_model,
29
+ "isDebug" => is_debug,
30
+ "appVersion" => app_version,
31
+ "sdkVersion" => sdk_version
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ # Turns POSIX locale strings like "en_US.UTF-8" into BCP 47-ish "en-US".
38
+ def detect_locale
39
+ raw = ENV["LC_ALL"] || ENV["LC_MESSAGES"] || ENV.fetch("LANG", nil)
40
+ return "en-US" if raw.nil? || raw.empty? || %w[C POSIX].include?(raw)
41
+
42
+ raw.split(".").first.tr("_", "-")
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+ require "uri"
7
+
8
+ module Aptabase
9
+ # Thin HTTP layer over Net::HTTP. Raises NetworkError for any failure so
10
+ # the client only has one error type to handle.
11
+ class Transport
12
+ EVENTS_PATH = "/api/v0/events"
13
+
14
+ def initialize(base_url:, app_key:, timeout: 30.0)
15
+ @uri = URI.parse(base_url.chomp("/") + EVENTS_PATH)
16
+ @app_key = app_key
17
+ @timeout = timeout
18
+ end
19
+
20
+ def post_events(payload)
21
+ request = Net::HTTP::Post.new(@uri)
22
+ request["App-Key"] = @app_key
23
+ request["Content-Type"] = "application/json"
24
+ request.body = JSON.generate(payload)
25
+
26
+ response = Net::HTTP.start(
27
+ @uri.host, @uri.port,
28
+ use_ssl: @uri.scheme == "https",
29
+ open_timeout: @timeout, read_timeout: @timeout, write_timeout: @timeout
30
+ ) { |http| http.request(request) }
31
+
32
+ return if response.is_a?(Net::HTTPSuccess)
33
+
34
+ raise NetworkError.new(
35
+ "HTTP error #{response.code}: #{response.body}",
36
+ status_code: response.code.to_i
37
+ )
38
+ rescue Timeout::Error, SystemCallError, SocketError, IOError, OpenSSL::SSL::SSLError => e
39
+ raise NetworkError, "Network error: #{e.message}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aptabase
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point matching the gem name, so Bundler's default require works
4
+ # with `gem "aptabase-ruby"`.
5
+ require_relative "aptabase"
data/lib/aptabase.rb ADDED
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "aptabase/version"
4
+ require_relative "aptabase/errors"
5
+ require_relative "aptabase/system_properties"
6
+ require_relative "aptabase/event"
7
+ require_relative "aptabase/session"
8
+ require_relative "aptabase/transport"
9
+ require_relative "aptabase/client"
10
+
11
+ # Module-level singleton API, the most common way to use the SDK:
12
+ #
13
+ # Aptabase.init("A-EU-1234567890", app_version: "1.2.3")
14
+ # Aptabase.track("app_started")
15
+ # Aptabase.track("purchase", { "product_id" => "abc123" })
16
+ #
17
+ # For multiple apps or explicit lifecycle control, use Aptabase::Client directly.
18
+ module Aptabase
19
+ class << self
20
+ attr_reader :client
21
+
22
+ # Logger used when `init` is not given an explicit one. Set to
23
+ # Rails.logger automatically by the railtie in Rails apps.
24
+ attr_accessor :default_logger
25
+
26
+ # Initialize the global client. Replaces (and stops) any previous one.
27
+ # Queued events are flushed automatically when the process exits.
28
+ def init(app_key, **options)
29
+ @client&.stop
30
+ options[:logger] = default_logger if default_logger && !options.key?(:logger)
31
+ @client = Client.new(app_key, **options)
32
+ register_at_exit
33
+ @client
34
+ end
35
+
36
+ # Track an event on the global client. Warns (once) and discards the
37
+ # event if `Aptabase.init` has not been called.
38
+ def track(event_name, props = nil)
39
+ client = @client
40
+ if client.nil?
41
+ warn_not_initialized
42
+ return
43
+ end
44
+ client.track(event_name, props)
45
+ end
46
+
47
+ # Synchronously send all queued events.
48
+ def flush
49
+ @client&.flush
50
+ nil
51
+ end
52
+
53
+ # Stop the global client, flushing any remaining events.
54
+ def stop
55
+ @client&.stop
56
+ @client = nil
57
+ end
58
+
59
+ private
60
+
61
+ def register_at_exit
62
+ return if @at_exit_registered
63
+
64
+ @at_exit_registered = true
65
+ at_exit { @client&.stop }
66
+ end
67
+
68
+ def warn_not_initialized
69
+ return if @warned_not_initialized
70
+
71
+ @warned_not_initialized = true
72
+ Kernel.warn("[aptabase] Aptabase.track called before Aptabase.init - event discarded")
73
+ end
74
+ end
75
+ end
76
+
77
+ require_relative "aptabase/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module Aptabase
6
+ module Generators
7
+ # Creates config/initializers/aptabase.rb.
8
+ #
9
+ # rails generate aptabase:install
10
+ class InstallGenerator < ::Rails::Generators::Base
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Creates an Aptabase initializer in config/initializers"
14
+
15
+ def copy_initializer
16
+ copy_file "aptabase.rb", "config/initializers/aptabase.rb"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Aptabase - privacy-first analytics (https://aptabase.com)
4
+ #
5
+ # Get your app key from the Aptabase dashboard and store it in
6
+ # credentials (bin/rails credentials:edit):
7
+ #
8
+ # aptabase:
9
+ # app_key: A-EU-xxxxxxxxxx
10
+ #
11
+ # or in the APTABASE_APP_KEY environment variable.
12
+ app_key = Rails.application.credentials.dig(:aptabase, :app_key) || ENV.fetch("APTABASE_APP_KEY", nil)
13
+
14
+ if app_key
15
+ Aptabase.init(
16
+ app_key,
17
+ app_version: ENV.fetch("APP_VERSION", "1.0.0"),
18
+ is_debug: !Rails.env.production?
19
+ # base_url: "https://analytics.example.com" # required for self-hosted (A-SH-*) keys
20
+ )
21
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aptabase-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tiny Cloud Ventures
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: logger
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Ruby and Rails SDK for Aptabase, privacy-first analytics for mobile,
27
+ desktop and web applications.
28
+ email:
29
+ - matthew@tinycloudventures.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - lib/aptabase-ruby.rb
38
+ - lib/aptabase.rb
39
+ - lib/aptabase/client.rb
40
+ - lib/aptabase/errors.rb
41
+ - lib/aptabase/event.rb
42
+ - lib/aptabase/railtie.rb
43
+ - lib/aptabase/session.rb
44
+ - lib/aptabase/system_properties.rb
45
+ - lib/aptabase/transport.rb
46
+ - lib/aptabase/version.rb
47
+ - lib/generators/aptabase/install_generator.rb
48
+ - lib/generators/aptabase/templates/aptabase.rb
49
+ homepage: https://github.com/tiny-cloud-ventures/aptabase-ruby
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/tiny-cloud-ventures/aptabase-ruby
54
+ source_code_uri: https://github.com/tiny-cloud-ventures/aptabase-ruby
55
+ changelog_uri: https://github.com/tiny-cloud-ventures/aptabase-ruby/blob/main/CHANGELOG.md
56
+ rubygems_mfa_required: 'true'
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 3.1.0
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 4.0.6
72
+ specification_version: 4
73
+ summary: Ruby SDK for Aptabase - privacy-first analytics
74
+ test_files: []