Skip to content

feat: add service account support#152

Merged
lajohn4747 merged 29 commits into
masterfrom
johnla-multi-567-add-service-account-support-to-ruby-sdk
Jul 10, 2026
Merged

feat: add service account support#152
lajohn4747 merged 29 commits into
masterfrom
johnla-multi-567-add-service-account-support-to-ruby-sdk

Conversation

@lajohn4747

@lajohn4747 lajohn4747 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Service Account Credentials: New Mixpanel::ServiceAccountCredentials class for storing username, secret, and project_id
  • Import Support: Events#import now accepts service account credentials instead of API keys
  • Feature Flags Support: Both local and remote feature flags authenticate using service accounts
  • Readme.rdoc - Added usage examples
  • Comprehensive test coverage

@lajohn4747 lajohn4747 requested review from a team and rahul-mixpanel June 25, 2026 22:14
@linear-code

linear-code Bot commented Jun 25, 2026

Copy link
Copy Markdown

MULTI-567

@lajohn4747 lajohn4747 marked this pull request as draft June 25, 2026 22:15
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.41270% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.23%. Comparing base (3f9a8d4) to head (412fa9f).

Files with missing lines Patch % Lines
lib/mixpanel-ruby/tracker.rb 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #152      +/-   ##
==========================================
+ Coverage   96.73%   97.23%   +0.50%     
==========================================
  Files          14       15       +1     
  Lines         673      723      +50     
==========================================
+ Hits          651      703      +52     
+ Misses         22       20       -2     
Flag Coverage Δ
openfeature 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The as_json method alone is not sufficient for direct .to_json calls.
Ruby's JSON library requires an explicit to_json method to properly
serialize custom objects.
@lajohn4747 lajohn4747 changed the title Add service account support feat: add service account support Jun 26, 2026
Allow passing credentials directly to Tracker.new instead of requiring
them to be nested in the config hash. This provides a cleaner API:

Before:
  tracker = Tracker.new(token, nil,
    local_flags_config: { credentials: creds },
    remote_flags_config: { credentials: creds })

After:
  tracker = Tracker.new(token, nil,
    credentials: creds,
    local_flags_config: {},
    remote_flags_config: {})

Credentials in config still take precedence if both are provided,
allowing different credentials per provider if needed.
Credentials don't need to be part of the config hash - they're a
fundamental authentication parameter. This simplifies the API:

- Credentials are passed directly as a parameter to the flags providers
- No more confusing config[:credentials] nesting
- Config hash is only for provider-specific settings (api_host, timeouts, etc.)
- Cleaner separation of concerns

This removes 30+ lines of unnecessary complexity.
Update test setup to pass nil for credentials parameter in the
new 5-argument signature.
@lajohn4747 lajohn4747 marked this pull request as ready for review June 30, 2026 16:01
@lajohn4747 lajohn4747 requested review from efahk and tylerjroach June 30, 2026 16:01
@tylerjroach tylerjroach requested a review from Copilot June 30, 2026 16:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class “service account” support across import and feature flags by introducing a credentials object, wiring it through the tracker/providers, and updating docs + tests to demonstrate and validate the new flow.

Changes:

  • Introduce Mixpanel::ServiceAccountCredentials and serialize it into import messages.
  • Update import pipeline (Events#importConsumer#send!) to send either legacy api_key or service account fields.
  • Thread optional credentials into local/remote flags providers and use them for HTTP Basic Auth, plus README usage examples and new/updated specs.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
spec/mixpanel-ruby/tracker_spec.rb Adds spec asserting credentials are passed into flags providers.
spec/mixpanel-ruby/flags/remote_flags_spec.rb Updates constructor usage and adds auth test for remote flags with credentials.
spec/mixpanel-ruby/flags/local_flags_spec.rb Updates constructor usage and adds auth test for local flags with credentials.
spec/mixpanel-ruby/events_spec.rb Adds import message-shape test for service account credentials (currently missing a require).
spec/mixpanel-ruby/credentials_spec.rb New unit tests for credentials validation + JSON serialization.
spec/mixpanel-ruby/consumer_spec.rb Adds test ensuring import requests include service account fields.
Readme.rdoc Documents service account usage for import and feature flags.
lib/mixpanel-ruby/tracker.rb Adds credentials: keyword and passes it to flags providers; updates import docs/signature.
lib/mixpanel-ruby/flags/remote_flags_provider.rb Extends initializer to accept credentials and forward into provider config.
lib/mixpanel-ruby/flags/local_flags_provider.rb Extends initializer to accept credentials and forward into provider config.
lib/mixpanel-ruby/flags/flags_provider.rb Uses credentials for Basic Auth when present; retains token auth fallback.
lib/mixpanel-ruby/events.rb Allows import to accept either API key or service account credentials.
lib/mixpanel-ruby/credentials.rb Implements ServiceAccountCredentials and JSON serialization helpers.
lib/mixpanel-ruby/consumer.rb Sends either service account fields or legacy API key when posting import payloads.
lib/mixpanel-ruby.rb Requires the new credentials file.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/mixpanel-ruby/flags/flags_provider.rb
Comment thread spec/mixpanel-ruby/events_spec.rb
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

Safe to merge — credentials never appear in serialized JSON, the auth flow is correct end-to-end, and the previously identified arity/initialization issues are resolved.

The credential handling design is sound: ServiceAccountCredentials stays on the consumer as an instance variable and only touches the HTTP layer, never the message payload. The two remaining findings are narrow edge cases that don't affect the main import or flags auth paths.

No files require special attention for merging.

Important Files Changed

Filename Overview
lib/mixpanel-ruby/credentials.rb New ServiceAccountCredentials class; Integer project_id correctly handled, but non-Integer/non-String types still cause NoMethodError on the empty? check.
lib/mixpanel-ruby/consumer.rb Credentials forwarded via keyword args preserving backward compat; BufferedConsumer missing the warning present in Events when credentials+block are combined.
lib/mixpanel-ruby/events.rb import/import_events split is clean; credentials kept out of serialized JSON; ArgumentError on missing auth is correct.
lib/mixpanel-ruby/flags/local_flags_provider.rb credentials added as trailing optional param, forwarded through provider_config to FlagsProvider base class correctly.
lib/mixpanel-ruby/tracker.rb credentials threaded through to Events and both flag providers; error_handler initialized via super before any flag provider construction.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant Tracker
    participant Events
    participant Consumer
    participant MixpanelAPI

    User->>Tracker: new(token, credentials: creds)
    Tracker->>Events: super(token, credentials: creds)
    Events->>Consumer: new(credentials: creds)

    User->>Tracker: import_events(distinct_id, event, props)
    Tracker->>Events: import_events(...)
    Events->>Events: import_internal(nil, ...)
    Note over Events: No api_key in message JSON
    Events->>Consumer: send!(:import, message_json)
    Consumer->>Consumer: request(endpoint, form_data, credentials: creds, type: :import)
    Note over Consumer: Basic Auth header added, project_id in query param
    Consumer->>MixpanelAPI: "POST /import?project_id=X Authorization: Basic base64(user:secret)"
    MixpanelAPI-->>Consumer: 200 OK
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant Tracker
    participant Events
    participant Consumer
    participant MixpanelAPI

    User->>Tracker: new(token, credentials: creds)
    Tracker->>Events: super(token, credentials: creds)
    Events->>Consumer: new(credentials: creds)

    User->>Tracker: import_events(distinct_id, event, props)
    Tracker->>Events: import_events(...)
    Events->>Events: import_internal(nil, ...)
    Note over Events: No api_key in message JSON
    Events->>Consumer: send!(:import, message_json)
    Consumer->>Consumer: request(endpoint, form_data, credentials: creds, type: :import)
    Note over Consumer: Basic Auth header added, project_id in query param
    Consumer->>MixpanelAPI: "POST /import?project_id=X Authorization: Basic base64(user:secret)"
    MixpanelAPI-->>Consumer: 200 OK
Loading

Reviews (18): Last reviewed commit: "Use a separate method for service creden..." | Re-trigger Greptile

Comment thread lib/mixpanel-ruby/consumer.rb Outdated
Comment thread lib/mixpanel-ruby/events.rb Outdated
@lajohn4747 lajohn4747 marked this pull request as draft June 30, 2026 22:01
The stub needs to include the project_id query parameter to match the actual request being made.
Comment thread lib/mixpanel-ruby/credentials.rb Outdated
Comment thread lib/mixpanel-ruby/flags/local_flags_provider.rb Outdated
@lajohn4747 lajohn4747 requested review from efahk and tylerjroach July 1, 2026 17:21
@tylerjroach

tylerjroach commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review: public API / backward-compatibility concerns

Most of the surface here is additive and compatible (Tracker#initialize gaining a credentials: kwarg, the import first-param rename, the new ServiceAccountCredentials class). Findings below, one real backward-compat break plus a consistency issue and a security decision to confirm.

1. 🔴 Consumer#request is now called with extra args on every send — breaks custom-consumer overrides

send! now calls request(endpoint, form_data, credentials, type) unconditionally, for all event types (track, profile updates, imports — everything).

Consumer#request is a public, changelog-documented extension point — it was added in 1.3.0 specifically so users could swap the HTTP layer:

== 1.3.0
* Added Consumer#request method, demo with Faraday integration

Anyone who followed that guidance has a 2-arg override:

class MyConsumer < Mixpanel::Consumer
  def request(endpoint, form_data)   # per the 1.3.0 Faraday pattern
    # ...
  end
end

After this change, that override raises ArgumentError: wrong number of arguments (given 4, expected 2) on the first send of any type — not just imports.

Suggested fix — only widen the call on the new service-account path, and branch at the call site so all existing traffic still calls request(endpoint, form_data) with exactly two args:

# send!
credentials = decoded_message["credentials"]
form_data = {"data" => data, "verbose" => 1}
form_data.merge!("api_key" => api_key) if api_key && !credentials

response_code, response_body =
  if credentials
    request(endpoint, form_data, credentials: credentials, type: type)  # new path only
  else
    request(endpoint, form_data)                                        # unchanged
  end
def request(endpoint, form_data, credentials: nil, type: nil)
  uri = URI(endpoint)
  if credentials && type == :import
    query = URI.decode_www_form(uri.query || '').to_h
    query['project_id'] = credentials['project_id']
    uri.query = URI.encode_www_form(query)
  end
  request = Net::HTTP::Post.new(uri.request_uri)
  request.set_form_data(form_data)
  request.basic_auth(credentials['username'], credentials['secret']) if credentials && type == :import
  # ...unchanged Net::HTTP setup...
end

This way the only remaining break is a new combination (custom request override + service-account import), which no existing code can depend on because service accounts don't exist yet — and it fails loudly on the opt-in feature rather than silently across 100% of traffic. Note it must branch at the call site: always passing credentials:/type: (even as nil) re-breaks 2-arg overrides.

2. 🟡 RemoteFlagsProvider#initialize puts credentials in the middle — inconsistent with LocalFlagsProvider

(Correcting my earlier take: this is not a customer-facing break.) Both providers are only ever instantiated internally by Tracker (the sole .new call sites), and there's no README/changelog pattern presenting them as direct-use or override points. The remote call site matches the middle-position signature, so it functions correctly.

The remaining issue is consistency: the two sibling classes ended up with different constructor shapes, and the tracker calls them differently (credentials last for local, third for remote):

# local  — credentials trailing + default
def initialize(token, config, tracker_callback, error_handler, credentials = nil)
# remote — credentials 3rd, required
def initialize(token, config, credentials, tracker_callback, error_handler)

Not blocking, but worth aligning remote to match local (credentials = nil trailing) and updating the remote call site — cheaper to fix now than to leave two divergent signatures. Looks like the patch-16 rebase aligned only the local provider.

3. 🟡 Service account secret is serialized into the message payload

ServiceAccountCredentials#as_json includes secret, so it rides inside the message JSON handed to the sink/consumer. The default Consumer needs it for Basic Auth and strips it from the POST body — fine there. But the secret is now present in the serialized message that any custom sink/block, BufferedConsumer, or async executor receives, and could log or persist in plaintext.

This flip-flopped during the branch (removed for security in one commit, re-added in another), so I want to make sure it's a deliberate, documented decision rather than an accident. If the secret should never touch the message channel, credentials would need to live on the consumer/tracker instance instead of in the payload — larger change, worth a separate discussion. At minimum, please document that messages carry the secret so operators don't log them.

@tylerjroach

Copy link
Copy Markdown
Contributor

Hey @lajohn4747 I posted a a Claude review guided by a few concerns I had noticed. Theres a few places where params were added that should probably be added to the end to ensure they aren't breaking. A few other concerns were posted as well. LMK what you think.

Comment thread lib/mixpanel-ruby/tracker.rb Outdated
Comment thread lib/mixpanel-ruby/tracker.rb
- Add tests for hash-based credentials (string and symbol keys)
- Add tests for credential validation error paths
- Add test for BufferedConsumer with credentials
- Add test for connection error handling
- Add test verifying api_key is not sent when credentials are present
- Coverage now at 96.85% (was 95.43%)
1. Add validation in Events#import to require authentication
   - Raises ArgumentError if both api_key and credentials are nil
   - Prevents confusing silent failures

2. Simplify Tracker#initialize to always call super
   - Properly initializes parent class in all cases
   - Overrides sink only when custom consumer provided
   - Fixes OOP principle violation

3. Add test for import without authentication
   - Verifies ArgumentError is raised appropriately

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Hash credentials were never used in the actual API flow:
- All constructors accept ServiceAccountCredentials or nil
- @credentials is always ServiceAccountCredentials or nil
- Hash path in Consumer#request was only exercised by tests that directly called request()

This removes unnecessary complexity and ~50 lines of dead code.

Also add coverage/ and vendor/ to .gitignore to prevent accidental commits.
@lajohn4747 lajohn4747 force-pushed the johnla-multi-567-add-service-account-support-to-ruby-sdk branch from 7ea44e7 to 51974d6 Compare July 9, 2026 16:43
@mixpanel mixpanel deleted a comment from greptile-apps Bot Jul 9, 2026
tylerjroach
tylerjroach previously approved these changes Jul 9, 2026

@tylerjroach tylerjroach left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No warning when both custom-consumer-block AND credentials are provided. Python logs a warning telling the user credentials will be ignored because they went to Mixpanel() instead of the custom consumer. Ruby silently ignores. Worth adding — 3-line change in Events#initialize:

if block && credentials
  warn '[WARNING] credentials passed to Tracker are ignored when a custom sink block is provided. Pass credentials to your consumer directly.'
end

No deprecation warning on legacy api_key path. Python logs a warning on import_data(api_key=...). Ruby doesn't. Greptile flagged this earlier — should be added, once per process to avoid stderr flooding.

@lajohn4747

Copy link
Copy Markdown
Contributor Author

Update to use a separate method import_events when using service account credentials. Deprecate import and it's API key usage

@lajohn4747 lajohn4747 requested a review from tylerjroach July 9, 2026 20:50
@lajohn4747 lajohn4747 merged commit aa32908 into master Jul 10, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants