mixpanel-ruby 3.1.0 → 3.2.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.
data/Readme.rdoc CHANGED
@@ -1,5 +1,7 @@
1
1
  = mixpanel-ruby: The official Mixpanel Ruby library
2
2
 
3
+ ##### _July 10, 2026_ - [v3.2.0](https://github.com/mixpanel/mixpanel-ruby/releases/tag/v3.2.0)
4
+
3
5
  mixpanel-ruby is a library for tracking events and sending \Mixpanel profile
4
6
  updates to \Mixpanel from your ruby applications.
5
7
 
@@ -27,6 +29,69 @@ The primary class you will use to track events is Mixpanel::Tracker. An instance
27
29
  Mixpanel::Tracker is enough to send events directly to \Mixpanel, and get you integrated
28
30
  right away.
29
31
 
32
+ == Service Account Authentication
33
+
34
+ Service accounts provide secure server-to-server authentication and are recommended over
35
+ API keys for import operations and feature flags.
36
+
37
+ === Import with Service Account (Recommended)
38
+
39
+ require 'mixpanel-ruby'
40
+
41
+ # Create service account credentials
42
+ credentials = Mixpanel::ServiceAccountCredentials.new(
43
+ 'your-service-account-username',
44
+ 'your-service-account-secret',
45
+ 'your-project-id'
46
+ )
47
+
48
+ # Pass credentials to tracker constructor
49
+ # Credentials are stored securely and never serialized to JSON
50
+ tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN, credentials: credentials)
51
+
52
+ # Import historical events using import_events (recommended)
53
+ tracker.import_events('User1', 'Past Event', {
54
+ 'time' => 1369353600,
55
+ 'Source' => 'Import'
56
+ })
57
+
58
+ # Alternative: use import() with nil as first parameter
59
+ tracker.import(nil, 'User1', 'Past Event', {
60
+ 'time' => 1369353600,
61
+ 'Source' => 'Import'
62
+ })
63
+
64
+ === Import with API Key (Legacy)
65
+
66
+ # Legacy approach with API key string
67
+ tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
68
+ tracker.import("API_KEY", 'User1', 'Past Event', {
69
+ 'time' => 1369353600,
70
+ 'Source' => 'Import'
71
+ })
72
+
73
+ === Feature Flags with Service Account
74
+
75
+ require 'mixpanel-ruby'
76
+
77
+ credentials = Mixpanel::ServiceAccountCredentials.new(
78
+ 'your-service-account-username',
79
+ 'your-service-account-secret',
80
+ 'your-project-id'
81
+ )
82
+
83
+ # Pass credentials directly to the tracker
84
+ tracker = Mixpanel::Tracker.new(
85
+ YOUR_MIXPANEL_TOKEN,
86
+ nil,
87
+ credentials: credentials,
88
+ local_flags_config: {},
89
+ remote_flags_config: {}
90
+ )
91
+
92
+ # Use feature flags
93
+ is_enabled = tracker.remote_flags.is_enabled?('my-flag', { 'distinct_id' => 'User1' })
94
+
30
95
  == Additional Information
31
96
 
32
97
  For more information please visit:
@@ -40,6 +40,11 @@ module Mixpanel
40
40
  # @kestrel.set(ANALYTICS_QUEUE, [type, message].to_json)
41
41
  # end
42
42
  #
43
+ # IMPORTANT SECURITY NOTE: Always pass credentials to the Consumer or Tracker
44
+ # constructor (credentials: ...). Credentials are stored as instance variables
45
+ # and used only for HTTP Basic Auth headers - they never appear in message JSON,
46
+ # preventing accidental credential leakage in logs or queue storage.
47
+ #
43
48
  # You can also instantiate the library consumers yourself, and use
44
49
  # them wherever you would like. For example, the working that
45
50
  # consumes the above queue might work like this:
@@ -58,14 +63,19 @@ module Mixpanel
58
63
  # they will be used instead of the default Mixpanel endpoints.
59
64
  # This can be useful for proxying, debugging, or if you prefer
60
65
  # not to use SSL for your events.
66
+ #
67
+ # @param credentials [ServiceAccountCredentials, nil] Service account credentials for authentication.
68
+ # Credentials are only used for the /import endpoint and feature flags.
61
69
  def initialize(events_endpoint=nil,
62
70
  update_endpoint=nil,
63
71
  groups_endpoint=nil,
64
- import_endpoint=nil)
72
+ import_endpoint=nil,
73
+ credentials: nil)
65
74
  @events_endpoint = events_endpoint || 'https://api.mixpanel.com/track'
66
75
  @update_endpoint = update_endpoint || 'https://api.mixpanel.com/engage'
67
76
  @groups_endpoint = groups_endpoint || 'https://api.mixpanel.com/groups'
68
77
  @import_endpoint = import_endpoint || 'https://api.mixpanel.com/import'
78
+ @credentials = credentials
69
79
  end
70
80
 
71
81
  # Send the given string message to Mixpanel. Type should be
@@ -85,13 +95,26 @@ module Mixpanel
85
95
 
86
96
  decoded_message = JSON.load(message)
87
97
  api_key = decoded_message["api_key"]
98
+
88
99
  data = Base64.encode64(decoded_message["data"].to_json).gsub("\n", '')
89
100
 
90
101
  form_data = {"data" => data, "verbose" => 1}
91
- form_data.merge!("api_key" => api_key) if api_key
102
+
103
+ # Only add api_key to form data if using legacy API key (not service account credentials)
104
+ # Service account credentials use HTTP Basic Auth instead
105
+ if api_key && !@credentials
106
+ form_data.merge!("api_key" => api_key)
107
+ end
92
108
 
93
109
  begin
94
- response_code, response_body = request(endpoint, form_data)
110
+ # Use keyword arguments for credentials to maintain backward compatibility
111
+ # with custom Consumer subclasses that override request(endpoint, form_data)
112
+ response_code, response_body =
113
+ if @credentials && type == :import
114
+ request(endpoint, form_data, credentials: @credentials, type: type)
115
+ else
116
+ request(endpoint, form_data)
117
+ end
95
118
  rescue => e
96
119
  raise ConnectionError.new("Could not connect to Mixpanel, with error \"#{e.message}\".")
97
120
  end
@@ -123,11 +146,32 @@ module Mixpanel
123
146
  #
124
147
  # as the result of the response. Response code should be nil if
125
148
  # the request never receives a response for some reason.
126
- def request(endpoint, form_data)
149
+ #
150
+ # For service account authentication, pass credentials (ServiceAccountCredentials object)
151
+ # and type (:import) as keyword arguments.
152
+ # The positional parameters are preserved for backward compatibility with custom Consumer subclasses.
153
+ def request(endpoint, form_data, credentials: nil, type: nil)
127
154
  uri = URI(endpoint)
155
+
156
+ # Add project_id as query parameter for import endpoint with service account credentials
157
+ if credentials && type == :import
158
+ unless credentials.is_a?(ServiceAccountCredentials)
159
+ raise ArgumentError, "credentials must be ServiceAccountCredentials, got #{credentials.class}"
160
+ end
161
+
162
+ query_params = URI.decode_www_form(uri.query || '').to_h
163
+ query_params['project_id'] = credentials.project_id
164
+ uri.query = URI.encode_www_form(query_params)
165
+ end
166
+
128
167
  request = Net::HTTP::Post.new(uri.request_uri)
129
168
  request.set_form_data(form_data)
130
169
 
170
+ # Use Basic Auth with service account credentials for import endpoint
171
+ if credentials && type == :import
172
+ request.basic_auth(credentials.username, credentials.secret)
173
+ end
174
+
131
175
  client = Net::HTTP.new(uri.host, uri.port)
132
176
  client.use_ssl = true
133
177
  client.open_timeout = 10
@@ -182,17 +226,21 @@ module Mixpanel
182
226
  # to the Mixpanel::Tracker constructor. If a block is passed to
183
227
  # the constructor, the *_endpoint constructor arguments are
184
228
  # ignored.
185
- def initialize(events_endpoint=nil, update_endpoint=nil, import_endpoint=nil, max_buffer_length=MAX_LENGTH, &block)
229
+ #
230
+ # @param credentials [ServiceAccountCredentials, nil] Service account credentials for authentication.
231
+ # Credentials are only used for the /import endpoint and feature flags.
232
+ def initialize(events_endpoint=nil, update_endpoint=nil, import_endpoint=nil, max_buffer_length=MAX_LENGTH, credentials: nil, &block)
186
233
  @max_length = [max_buffer_length, MAX_LENGTH].min
187
234
  @buffers = {
188
235
  :event => [],
189
236
  :profile_update => [],
190
237
  }
238
+ @credentials = credentials
191
239
 
192
240
  if block
193
241
  @sink = block
194
242
  else
195
- consumer = Consumer.new(events_endpoint, update_endpoint, import_endpoint)
243
+ consumer = Consumer.new(events_endpoint, update_endpoint, nil, import_endpoint, credentials: credentials)
196
244
  @sink = consumer.method(:send!)
197
245
  end
198
246
  end
@@ -0,0 +1,26 @@
1
+ module Mixpanel
2
+ # Service account credentials for server-to-server authentication
3
+ # This is the recommended authentication method over API keys
4
+ class ServiceAccountCredentials
5
+ attr_reader :username, :secret, :project_id
6
+
7
+ # Create service account credentials
8
+ # @param username [String] Service account username
9
+ # @param secret [String] Service account secret
10
+ # @param project_id [String, Integer] Mixpanel project ID (accepts string or integer)
11
+ def initialize(username, secret, project_id)
12
+ raise ArgumentError, 'username is required' if username.nil? || username.empty?
13
+ raise ArgumentError, 'secret is required' if secret.nil? || secret.empty?
14
+ raise ArgumentError, 'project_id is required' if project_id.nil?
15
+
16
+ # Convert project_id to string if it's an integer (Mixpanel dashboard shows numeric IDs)
17
+ project_id = project_id.to_s if project_id.is_a?(Integer)
18
+ raise ArgumentError, 'project_id is required' if project_id.empty?
19
+
20
+ @username = username
21
+ @secret = secret
22
+ @project_id = project_id
23
+ end
24
+
25
+ end
26
+ end
@@ -22,14 +22,19 @@ module Mixpanel
22
22
  # # tracker has all of the methods of Mixpanel::Events
23
23
  # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
24
24
  #
25
- def initialize(token, error_handler=nil, &block)
25
+ def initialize(token, error_handler=nil, credentials: nil, &block)
26
26
  @token = token
27
27
  @error_handler = error_handler || ErrorHandler.new
28
+ @credentials = credentials
29
+
30
+ if block && credentials
31
+ warn '[WARNING] credentials passed to Events/Tracker are ignored when a custom sink block is provided. Pass credentials to your consumer directly.'
32
+ end
28
33
 
29
34
  if block
30
35
  @sink = block
31
36
  else
32
- consumer = Consumer.new
37
+ consumer = Consumer.new(credentials: credentials)
33
38
  @sink = consumer.method(:send!)
34
39
  end
35
40
  end
@@ -87,19 +92,66 @@ module Mixpanel
87
92
  # we pass the time of the method call as the time the event occured, if you
88
93
  # wish to override this pass a timestamp in the properties hash.
89
94
  #
90
- # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
91
- #
92
- # # Track that user "12345"'s credit card was declined
93
- # tracker.import("API_KEY", "12345", "Credit Card Declined")
94
- #
95
- # # Properties describe the circumstances of the event,
96
- # # or aspects of the source or user associated with the event
97
- # tracker.import("API_KEY", "12345", "Welcome Email Sent", {
95
+ # # Recommended: Use import_events() instead
96
+ # credentials = Mixpanel::ServiceAccountCredentials.new(username, secret, project_id)
97
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN, credentials: credentials)
98
+ # tracker.import_events("12345", "Welcome Email Sent", {
98
99
  # 'Email Template' => 'Pretty Pink Welcome',
99
100
  # 'User Sign-up Cohort' => 'July 2013',
100
101
  # 'time' => 1369353600,
101
102
  # })
103
+ #
104
+ # # Legacy: Pass API key as first parameter (deprecated)
105
+ # tracker.import("API_KEY", "12345", "Credit Card Declined")
106
+ #
102
107
  def import(api_key, distinct_id, event, properties={}, ip=nil)
108
+ # Warn about deprecated API key usage
109
+ if api_key && !api_key.to_s.empty?
110
+ warn '[DEPRECATION] Passing api_key to import() is deprecated. Use ServiceAccountCredentials in the constructor instead. See https://docs.mixpanel.com/docs/tracking-methods/sdks/ruby#service-account-authentication'
111
+ end
112
+
113
+ # Warn when using import(nil, ...) - recommend import_events instead
114
+ if api_key.nil? && @credentials
115
+ warn '[DEPRECATION] Using import(nil, ...) is deprecated. Use import_events(...) instead for cleaner API.'
116
+ end
117
+
118
+ # Delegate to internal implementation
119
+ import_internal(api_key, distinct_id, event, properties, ip)
120
+ end
121
+
122
+ # Import an event using service account credentials from the constructor.
123
+ # This is the recommended method for importing historical events with service accounts.
124
+ #
125
+ # Credentials must be provided in the Tracker/Events constructor. This method is cleaner
126
+ # than import() as it doesn't require passing nil as the first parameter.
127
+ #
128
+ # credentials = Mixpanel::ServiceAccountCredentials.new(username, secret, project_id)
129
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN, credentials: credentials)
130
+ #
131
+ # tracker.import_events('user123', 'Past Event', {
132
+ # 'time' => 1369353600,
133
+ # 'Source' => 'Import'
134
+ # })
135
+ #
136
+ def import_events(distinct_id, event, properties={}, ip=nil)
137
+ unless @credentials
138
+ raise ArgumentError, 'import_events requires credentials in constructor. Use: Tracker.new(token, credentials: credentials)'
139
+ end
140
+
141
+ # Delegate to internal implementation with nil api_key (uses constructor credentials)
142
+ import_internal(nil, distinct_id, event, properties, ip)
143
+ end
144
+
145
+ private
146
+
147
+ # Internal implementation for importing events.
148
+ # Called by both import() and import_events().
149
+ def import_internal(api_key, distinct_id, event, properties, ip)
150
+ # Validate that at least one authentication method is provided
151
+ if api_key.nil? && @credentials.nil?
152
+ raise ArgumentError, 'import requires authentication: provide either api_key parameter or credentials in constructor'
153
+ end
154
+
103
155
  properties = {
104
156
  'distinct_id' => distinct_id,
105
157
  'token' => @token,
@@ -116,9 +168,15 @@ module Mixpanel
116
168
 
117
169
  message = {
118
170
  'data' => data,
119
- 'api_key' => api_key,
120
171
  }
121
172
 
173
+ # Only include api_key in message if provided (legacy auth)
174
+ # When using service account credentials (recommended), pass nil for api_key
175
+ # and the Consumer will use credentials from its instance variable
176
+ if api_key
177
+ message['api_key'] = api_key
178
+ end
179
+
122
180
  ret = true
123
181
  begin
124
182
  @sink.call(:import, message.to_json)
@@ -12,7 +12,7 @@ module Mixpanel
12
12
  # Base class for feature flags providers
13
13
  # Provides common HTTP handling and exposure event tracking
14
14
  class FlagsProvider
15
- # @param provider_config [Hash] Configuration with :token, :api_host, :request_timeout_in_seconds
15
+ # @param provider_config [Hash] Configuration with :token, :api_host, :request_timeout_in_seconds, :credentials (optional)
16
16
  # @param endpoint [String] API endpoint path (e.g., '/flags' or '/flags/definitions')
17
17
  # @param tracker_callback [Proc] Function used to track events (bound tracker.track method)
18
18
  # @param evaluation_mode [String] The feature flag evaluation mode. This is either 'local' or 'remote'
@@ -23,6 +23,7 @@ module Mixpanel
23
23
  @tracker_callback = tracker_callback
24
24
  @evaluation_mode = evaluation_mode
25
25
  @error_handler = error_handler
26
+ @credentials = provider_config[:credentials]
26
27
  end
27
28
 
28
29
  # Make HTTP request to flags API endpoint
@@ -31,12 +32,20 @@ module Mixpanel
31
32
  # @raise [Mixpanel::ConnectionError] on network errors
32
33
  # @raise [Mixpanel::ServerError] on HTTP errors
33
34
  def call_flags_endpoint(additional_params = nil)
35
+ # Always use token in query params
34
36
  common_params = Utils.prepare_common_query_params(
35
37
  @provider_config[:token],
36
38
  Mixpanel::VERSION
37
39
  )
38
40
 
39
41
  params = common_params.merge(additional_params || {})
42
+
43
+ # Add project_id as query parameter when using service account credentials
44
+ # Note: project_id is required for service account auth but not used for token auth
45
+ if @credentials
46
+ params['project_id'] = @credentials.project_id
47
+ end
48
+
40
49
  query_string = URI.encode_www_form(params)
41
50
 
42
51
  uri = URI::HTTPS.build(
@@ -53,7 +62,12 @@ module Mixpanel
53
62
 
54
63
  request = Net::HTTP::Get.new(uri.request_uri)
55
64
 
56
- request.basic_auth(@provider_config[:token], '')
65
+ # Use service account credentials for basic auth if provided, otherwise use token
66
+ if @credentials
67
+ request.basic_auth(@credentials.username, @credentials.secret)
68
+ else
69
+ request.basic_auth(@provider_config[:token], '')
70
+ end
57
71
 
58
72
  request['Content-Type'] = 'application/json'
59
73
  request['traceparent'] = Utils.generate_traceparent
@@ -18,13 +18,26 @@ module Mixpanel
18
18
  # @param config [Hash] Local flags configuration
19
19
  # @param tracker_callback [Proc] Callback to track events
20
20
  # @param error_handler [Mixpanel::ErrorHandler] Error handler
21
- def initialize(token, config, tracker_callback, error_handler)
22
- @config = DEFAULT_CONFIG.merge(config || {})
21
+ # @param credentials [ServiceAccountCredentials, nil] Optional service account credentials
22
+ def initialize(token, config, tracker_callback, error_handler, credentials = nil)
23
+ # compact: an explicit nil from the caller (e.g.
24
+ # polling_interval_in_seconds: nil) must not override a sane default.
25
+ # Both the previous sleep(nil) and the current
26
+ # ConditionVariable#wait(mutex, nil) block indefinitely, which would
27
+ # silently disable polling — fall back to the default instead.
28
+ @config = DEFAULT_CONFIG.merge((config || {}).compact)
29
+
30
+ interval = @config[:polling_interval_in_seconds]
31
+ unless interval.is_a?(Numeric) && interval > 0
32
+ raise ArgumentError,
33
+ "polling_interval_in_seconds must be a positive number, got: #{interval.inspect}"
34
+ end
23
35
 
24
36
  provider_config = {
25
37
  token: token,
26
38
  api_host: @config[:api_host],
27
- request_timeout_in_seconds: @config[:request_timeout_in_seconds]
39
+ request_timeout_in_seconds: @config[:request_timeout_in_seconds],
40
+ credentials: credentials
28
41
  }
29
42
 
30
43
  super(provider_config, '/flags/definitions', tracker_callback, 'local', error_handler)
@@ -32,36 +45,80 @@ module Mixpanel
32
45
  @flag_definitions = {}
33
46
  @polling_thread = nil
34
47
  @stop_polling = false
48
+ @polling_mutex = Mutex.new
49
+ @polling_condition = ConditionVariable.new
50
+ # Separate from @polling_mutex: serializes the full start/stop lifecycle
51
+ # (including the join inside stop) so a concurrent start can't observe a
52
+ # mid-stop state and spawn a new polling thread before the old one
53
+ # finishes exiting. We can't hold @polling_mutex across the join — the
54
+ # polling thread needs it to wake from the condition wait.
55
+ @lifecycle_mutex = Mutex.new
35
56
  end
36
57
 
37
58
  # Start polling for flag definitions
38
59
  # Fetches immediately, then at regular intervals if polling enabled
39
60
  def start_polling_for_definitions!
61
+ # Clear @stop_polling BEFORE the initial fetch so a concurrent
62
+ # stop_polling_for_definitions! arriving during the fetch can flip it
63
+ # back to true and signal "abandon start". Under @polling_mutex, writes
64
+ # to @stop_polling are linearized: whichever of {start's clear, stop's
65
+ # set} happened latest is what we observe under the lifecycle lock
66
+ # below — i.e. last-writer-wins semantics preserve stop's postcondition
67
+ # (polling is off when stop returns) even across the unguarded fetch.
68
+ @polling_mutex.synchronize { @stop_polling = false }
69
+
70
+ # Initial fetch is intentionally outside @lifecycle_mutex: it's a
71
+ # blocking HTTP call, and holding the lifecycle lock across it would
72
+ # block a concurrent stop_polling_for_definitions! for the full request
73
+ # timeout.
40
74
  fetch_flag_definitions
41
75
 
42
- if @config[:enable_polling] && !@polling_thread
43
- @stop_polling = false
44
- @polling_thread = Thread.new do
45
- loop do
46
- sleep @config[:polling_interval_in_seconds]
47
- break if @stop_polling
48
-
49
- begin
50
- fetch_flag_definitions
51
- rescue StandardError => e
52
- @error_handler.handle(e) if @error_handler
76
+ @lifecycle_mutex.synchronize do
77
+ # If a stop arrived during/after our @stop_polling clear above, abort
78
+ # the user's stop "wins" because it's the most recent intent.
79
+ stop_requested = @polling_mutex.synchronize { @stop_polling }
80
+
81
+ # .alive? guards against a prior polling thread that died abnormally
82
+ # (e.g. error_handler itself raised); without it @polling_thread would
83
+ # be non-nil-but-dead and we'd silently refuse to restart polling.
84
+ if !stop_requested && @config[:enable_polling] &&
85
+ (@polling_thread.nil? || !@polling_thread.alive?)
86
+ @polling_thread = Thread.new do
87
+ loop do
88
+ # Check @stop_polling INSIDE the mutex (before and after wait)
89
+ # so a broadcast from stop_polling_for_definitions! can't be
90
+ # lost if it arrives while we're outside the synchronized region
91
+ # (e.g. during fetch_flag_definitions below).
92
+ stopped = @polling_mutex.synchronize do
93
+ next true if @stop_polling
94
+
95
+ @polling_condition.wait(@polling_mutex, @config[:polling_interval_in_seconds])
96
+ @stop_polling
97
+ end
98
+ break if stopped
99
+
100
+ begin
101
+ fetch_flag_definitions
102
+ rescue StandardError => e
103
+ safe_handle_error(e)
104
+ end
53
105
  end
54
106
  end
55
107
  end
56
108
  end
57
109
  rescue StandardError => e
58
- @error_handler.handle(e) if @error_handler
110
+ safe_handle_error(e)
59
111
  end
60
112
 
61
113
  def stop_polling_for_definitions!
62
- @stop_polling = true
63
- @polling_thread&.join
64
- @polling_thread = nil
114
+ @lifecycle_mutex.synchronize do
115
+ @polling_mutex.synchronize do
116
+ @stop_polling = true
117
+ @polling_condition.broadcast
118
+ end
119
+ @polling_thread&.join
120
+ @polling_thread = nil
121
+ end
65
122
  end
66
123
 
67
124
  def shutdown
@@ -141,6 +198,16 @@ module Mixpanel
141
198
 
142
199
  private
143
200
 
201
+ # Wrap @error_handler.handle so a misbehaving handler can't kill the
202
+ # polling thread mid-loop — that would leave @polling_thread non-nil but
203
+ # dead, and (without the .alive? check in start) silently prevent restart.
204
+ def safe_handle_error(error)
205
+ @error_handler.handle(error) if @error_handler
206
+ rescue StandardError
207
+ # Swallow: keeping the polling loop alive is more important than
208
+ # propagating a broken handler's failure.
209
+ end
210
+
144
211
  def fetch_flag_definitions
145
212
  response = call_flags_endpoint
146
213
 
@@ -14,13 +14,15 @@ module Mixpanel
14
14
  # @param config [Hash] Remote flags configuration
15
15
  # @param tracker_callback [Proc] Callback to track events
16
16
  # @param error_handler [Mixpanel::ErrorHandler] Error handler
17
- def initialize(token, config, tracker_callback, error_handler)
17
+ # @param credentials [ServiceAccountCredentials, nil] Optional service account credentials
18
+ def initialize(token, config, tracker_callback, error_handler, credentials = nil)
18
19
  merged_config = DEFAULT_CONFIG.merge(config || {})
19
20
 
20
21
  provider_config = {
21
22
  token: token,
22
23
  api_host: merged_config[:api_host],
23
- request_timeout_in_seconds: merged_config[:request_timeout_in_seconds]
24
+ request_timeout_in_seconds: merged_config[:request_timeout_in_seconds],
25
+ credentials: credentials
24
26
  }
25
27
 
26
28
  super(provider_config, '/flags', tracker_callback, 'remote', error_handler)
@@ -62,9 +62,14 @@ module Mixpanel
62
62
  # If a block is provided, it is passed a type (one of :event or :profile_update)
63
63
  # and a string message. This same format is accepted by Mixpanel::Consumer#send!
64
64
  # and Mixpanel::BufferedConsumer#send!
65
- def initialize(token, error_handler=nil, local_flags_config: nil, remote_flags_config: nil, &block)
66
- super(token, error_handler, &block)
67
- @token = token
65
+ #
66
+ # Optional parameters:
67
+ # - credentials: ServiceAccountCredentials for authentication (used for import and feature flags)
68
+ # - local_flags_config: Configuration hash for local feature flags
69
+ # - remote_flags_config: Configuration hash for remote feature flags
70
+ def initialize(token, error_handler=nil, credentials: nil, local_flags_config: nil, remote_flags_config: nil, &block)
71
+ super(token, error_handler, credentials: credentials, &block)
72
+
68
73
  @people = People.new(token, error_handler, &block)
69
74
  @groups = Groups.new(token, error_handler, &block)
70
75
 
@@ -74,7 +79,8 @@ module Mixpanel
74
79
  token,
75
80
  local_flags_config,
76
81
  method(:track), # Pass bound method as callback
77
- error_handler || ErrorHandler.new
82
+ error_handler || ErrorHandler.new,
83
+ credentials
78
84
  )
79
85
  end
80
86
 
@@ -84,7 +90,8 @@ module Mixpanel
84
90
  token,
85
91
  remote_flags_config,
86
92
  method(:track), # Pass bound method as callback
87
- error_handler || ErrorHandler.new
93
+ error_handler || ErrorHandler.new,
94
+ credentials
88
95
  )
89
96
  end
90
97
  end
@@ -122,14 +129,15 @@ module Mixpanel
122
129
  #
123
130
  # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN)
124
131
  #
125
- # # Import event that user "12345"'s credit card was declined
132
+ # # Using deprecated API key (still supported)
126
133
  # tracker.import("API_KEY", "12345", "Credit Card Declined", {
127
134
  # 'time' => 1310111365
128
135
  # })
129
136
  #
130
- # # Properties describe the circumstances of the event,
131
- # # or aspects of the source or user associated with the event
132
- # tracker.import("API_KEY", "12345", "Welcome Email Sent", {
137
+ # # Using service account credentials (recommended)
138
+ # credentials = Mixpanel::ServiceAccountCredentials.new(username, secret, project_id)
139
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN, credentials: credentials)
140
+ # tracker.import(nil, "12345", "Welcome Email Sent", {
133
141
  # 'Email Template' => 'Pretty Pink Welcome',
134
142
  # 'User Sign-up Cohort' => 'July 2013',
135
143
  # 'time' => 1310111365
@@ -140,6 +148,28 @@ module Mixpanel
140
148
  super
141
149
  end
142
150
 
151
+ # Import an event using service account credentials from the constructor.
152
+ # This is the recommended method for importing historical events with service accounts.
153
+ #
154
+ # Service account credentials must be provided when creating the Tracker.
155
+ # This method provides a cleaner API than import() as it doesn't require
156
+ # passing nil as the first parameter.
157
+ #
158
+ # credentials = Mixpanel::ServiceAccountCredentials.new(username, secret, project_id)
159
+ # tracker = Mixpanel::Tracker.new(YOUR_MIXPANEL_TOKEN, credentials: credentials)
160
+ #
161
+ # # Import a historical event
162
+ # tracker.import_events('user123', 'Past Event', {
163
+ # 'Email Template' => 'Welcome Email',
164
+ # 'time' => 1369353600
165
+ # })
166
+ #
167
+ def import_events(distinct_id, event, properties={}, ip=nil)
168
+ # This is here strictly to allow rdoc to include the relevant
169
+ # documentation
170
+ super
171
+ end
172
+
143
173
  # Creates a distinct_id alias. \Events and updates with an alias
144
174
  # will be considered by mixpanel to have the same source, and
145
175
  # refer to the same profile.
@@ -1,3 +1,3 @@
1
1
  module Mixpanel
2
- VERSION = '3.1.0'
2
+ VERSION = '3.2.0'
3
3
  end
data/lib/mixpanel-ruby.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'mixpanel-ruby/consumer.rb'
2
2
  require 'mixpanel-ruby/tracker.rb'
3
3
  require 'mixpanel-ruby/version.rb'
4
+ require 'mixpanel-ruby/credentials.rb'
4
5
  require 'mixpanel-ruby/flags/utils.rb'
5
6
  require 'mixpanel-ruby/flags/types.rb'
6
7
  require 'mixpanel-ruby/flags/flags_provider.rb'
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [openfeature/v0.1.0](https://github.com/mixpanel/mixpanel-ruby/tree/openfeature/v0.1.0) (2026-05-13)
4
+
5
+ Initial release of the Mixpanel OpenFeature provider for Ruby.
@@ -1,5 +1,7 @@
1
1
  # mixpanel-ruby-openfeature
2
2
 
3
+ ##### _May 13, 2026_ - [openfeature/v0.1.0](https://github.com/mixpanel/mixpanel-ruby/releases/tag/openfeature/v0.1.0)
4
+
3
5
  [![Gem Version](https://img.shields.io/gem/v/mixpanel-ruby-openfeature.svg)](https://rubygems.org/gems/mixpanel-ruby-openfeature)
4
6
  [![OpenFeature](https://img.shields.io/badge/OpenFeature-compatible-green)](https://openfeature.dev/)
5
7
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/mixpanel/mixpanel-ruby/blob/master/LICENSE)