From 75bde573142688ad1c9ccfcfa333f46a9de4d47e Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:08:05 +0000 Subject: [PATCH 01/23] Add Service Account support --- packages/mixpanel/example-service-account.js | 116 +++++++++++++++++++ packages/mixpanel/lib/credentials.js | 77 ++++++++++++ packages/mixpanel/lib/flags/flags.js | 46 ++++++-- packages/mixpanel/lib/flags/local_flags.js | 12 +- packages/mixpanel/lib/flags/remote_flags.js | 5 +- packages/mixpanel/lib/flags/utils.js | 18 ++- packages/mixpanel/lib/mixpanel-node.d.ts | 10 ++ packages/mixpanel/lib/mixpanel-node.js | 42 ++++++- packages/mixpanel/readme.md | 45 ++++++- packages/mixpanel/test/import.js | 106 +++++++++++++++++ packages/mixpanel/test/send_request.js | 2 +- 11 files changed, 459 insertions(+), 20 deletions(-) create mode 100644 packages/mixpanel/example-service-account.js create mode 100644 packages/mixpanel/lib/credentials.js diff --git a/packages/mixpanel/example-service-account.js b/packages/mixpanel/example-service-account.js new file mode 100644 index 00000000..15d48b8c --- /dev/null +++ b/packages/mixpanel/example-service-account.js @@ -0,0 +1,116 @@ +/** + * Example usage of Service Account authentication with mixpanel-node + * + * Service accounts provide enhanced security for server-to-server integrations + * by using unique username/secret pairs instead of shared API secrets. + */ + +const Mixpanel = require("./lib/mixpanel-node"); +const { ServiceAccountCredentials } = Mixpanel; + +// Create service account credentials +// Replace with your actual service account credentials +const credentials = new ServiceAccountCredentials( + "YOUR_SERVICE_ACCOUNT_USERNAME", + "YOUR_SERVICE_ACCOUNT_SECRET", + "YOUR_PROJECT_ID", +); + +// Initialize Mixpanel with service account credentials +const mixpanel = Mixpanel.init("YOUR_PROJECT_TOKEN", { + credentials, + test: true, // Set to false in production + debug: true, // Enable debug logging +}); + +console.log("Service Account Example"); +console.log("=======================\n"); + +// Example 1: Import an old event +console.log("1. Importing an old event..."); +mixpanel.import( + "old_signup", + new Date(2020, 0, 1, 10, 30, 0), + { + distinct_id: "user123", + source: "historical_data", + plan: "premium", + }, + (err) => { + if (err) { + console.error(" Error importing event:", err.message); + } else { + console.log(" ✓ Event imported successfully"); + } + }, +); + +// Example 2: Import multiple events in a batch +console.log("\n2. Importing a batch of events..."); +mixpanel.import_batch( + [ + { + event: "page_view", + properties: { + time: new Date(2020, 0, 15, 14, 0, 0), + distinct_id: "user123", + page: "/home", + }, + }, + { + event: "page_view", + properties: { + time: new Date(2020, 0, 15, 14, 5, 0), + distinct_id: "user123", + page: "/products", + }, + }, + { + event: "purchase", + properties: { + time: new Date(2020, 0, 15, 14, 10, 0), + distinct_id: "user123", + amount: 99.99, + }, + }, + ], + (errors) => { + if (errors) { + console.error(" Errors importing batch:", errors); + } else { + console.log(" ✓ Batch imported successfully"); + } + }, +); + +// Example 3: Regular tracking (uses token in payload, no auth needed) +console.log("\n3. Tracking a current event..."); +mixpanel.track( + "button_clicked", + { + distinct_id: "user456", + button_name: "signup", + page: "/landing", + }, + (err) => { + if (err) { + console.error(" Error tracking event:", err.message); + } else { + console.log(" ✓ Event tracked successfully"); + } + }, +); + +// Example 4: Using with feature flags +console.log("\n4. Service accounts can also be used with feature flags"); +console.log(" (requires local_flags_config or remote_flags_config)"); + +console.log("\nNotes:"); +console.log( + "- Service account auth is only used for /import endpoint and feature flags", +); +console.log( + "- Regular tracking (/track, /engage, /groups) uses token in payload only", +); +console.log("- HTTPS is required when using service account credentials"); +console.log("- This example uses test mode - set test: false in production"); diff --git a/packages/mixpanel/lib/credentials.js b/packages/mixpanel/lib/credentials.js new file mode 100644 index 00000000..c142ac5e --- /dev/null +++ b/packages/mixpanel/lib/credentials.js @@ -0,0 +1,77 @@ +/** + * Service account credentials for server-to-server authentication. + * + * Service account authentication is the recommended method for server-side + * integrations. It provides better security than API secrets by using + * unique username/secret pairs instead of a single shared secret. + * + * @example + * const { ServiceAccountCredentials } = require('mixpanel/lib/credentials'); + * + * const credentials = new ServiceAccountCredentials( + * 'your-service-account-username', + * 'your-service-account-secret', + * '123456' + * ); + * const mixpanel = Mixpanel.init('YOUR_TOKEN', { credentials }); + */ +class ServiceAccountCredentials { + /** + * Create service account credentials. + * + * @param {string} username - Service account username + * @param {string} secret - Service account secret + * @param {string} project_id - Mixpanel project ID + * @throws {TypeError} If any parameter is not a string + * @throws {ValueError} If any parameter is empty or whitespace-only + */ + constructor(username, secret, project_id) { + if (typeof username !== "string") { + throw new TypeError("Service account username must be a string"); + } + if (typeof secret !== "string") { + throw new TypeError("Service account secret must be a string"); + } + if (typeof project_id !== "string") { + throw new TypeError("Service account project_id must be a string"); + } + + const trimmedUsername = username.trim(); + const trimmedSecret = secret.trim(); + const trimmedProjectId = project_id.trim(); + + if (!trimmedUsername) { + throw new Error("Service account username cannot be empty"); + } + if (!trimmedSecret) { + throw new Error("Service account secret cannot be empty"); + } + if (!trimmedProjectId) { + throw new Error("Service account project_id cannot be empty"); + } + + this.username = trimmedUsername; + this.secret = trimmedSecret; + this.project_id = trimmedProjectId; + } + + /** + * Convert credentials to HTTP Basic Auth format. + * + * @returns {string} Base64-encoded username:secret for Authorization header + */ + toHttpBasicAuth() { + return Buffer.from(this.username + ":" + this.secret).toString("base64"); + } + + /** + * String representation of credentials (masks secret). + * + * @returns {string} + */ + toString() { + return `ServiceAccountCredentials(username=${this.username}, project_id=${this.project_id}, secret=***)`; + } +} + +module.exports = { ServiceAccountCredentials }; diff --git a/packages/mixpanel/lib/flags/flags.js b/packages/mixpanel/lib/flags/flags.js index 9520e170..b7977cd3 100644 --- a/packages/mixpanel/lib/flags/flags.js +++ b/packages/mixpanel/lib/flags/flags.js @@ -23,13 +23,22 @@ class FeatureFlagsProvider { * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) * @param {string} evaluationMode - The feature flag evaluation mode * @param {CustomLogger} logger - Logger instance + * @param {Object} credentials - Optional service account credentials */ - constructor(providerConfig, endpoint, tracker, evaluationMode, logger) { + constructor( + providerConfig, + endpoint, + tracker, + evaluationMode, + logger, + credentials, + ) { this.providerConfig = providerConfig; this.endpoint = endpoint; this.tracker = tracker; this.evaluationMode = evaluationMode; this.logger = logger; + this.credentials = credentials; } /** @@ -39,10 +48,33 @@ class FeatureFlagsProvider { */ async callFlagsEndpoint(additionalParams = null) { return new Promise((resolve, reject) => { - const commonParams = prepareCommonQueryParams( - this.providerConfig.token, - packageInfo.version, - ); + let authHeader; + let commonParams; + + // Determine auth method + if (this.credentials && this.credentials.username) { + // Service account auth + commonParams = prepareCommonQueryParams( + null, // No token + packageInfo.version, + this.credentials.project_id, + ); + authHeader = + "Basic " + + Buffer.from( + this.credentials.username + ":" + this.credentials.secret, + ).toString("base64"); + } else { + // Token auth (existing) + commonParams = prepareCommonQueryParams( + this.providerConfig.token, + packageInfo.version, + ); + authHeader = + "Basic " + + Buffer.from(this.providerConfig.token + ":").toString("base64"); + } + const params = new URLSearchParams(commonParams); if (additionalParams) { @@ -60,9 +92,7 @@ class FeatureFlagsProvider { method: "GET", headers: { ...REQUEST_HEADERS, - Authorization: - "Basic " + - Buffer.from(this.providerConfig.token + ":").toString("base64"), + Authorization: authHeader, traceparent: generateTraceparent(), }, timeout: this.providerConfig.request_timeout_in_seconds * 1000, diff --git a/packages/mixpanel/lib/flags/local_flags.js b/packages/mixpanel/lib/flags/local_flags.js index f8d37728..712373ac 100644 --- a/packages/mixpanel/lib/flags/local_flags.js +++ b/packages/mixpanel/lib/flags/local_flags.js @@ -25,8 +25,9 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { * @param {LocalFlagsConfig} config - Local flags configuration * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) * @param {CustomLogger} logger - Logger + * @param {Object} credentials - Optional service account credentials */ - constructor(token, config, tracker, logger) { + constructor(token, config, tracker, logger, credentials) { const mergedConfig = { api_host: "api.mixpanel.com", request_timeout_in_seconds: 10, @@ -41,7 +42,14 @@ class LocalFeatureFlagsProvider extends FeatureFlagsProvider { request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, }; - super(providerConfig, "/flags/definitions", tracker, "local", logger); + super( + providerConfig, + "/flags/definitions", + tracker, + "local", + logger, + credentials, + ); this.config = mergedConfig; this.flagDefinitions = new Map(); diff --git a/packages/mixpanel/lib/flags/remote_flags.js b/packages/mixpanel/lib/flags/remote_flags.js index 5566916e..ee327021 100644 --- a/packages/mixpanel/lib/flags/remote_flags.js +++ b/packages/mixpanel/lib/flags/remote_flags.js @@ -18,8 +18,9 @@ class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { * @param {RemoteFlagsConfig} config - Remote flags configuration * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) * @param {CustomLogger} logger - Logger instance + * @param {Object} credentials - Optional service account credentials */ - constructor(token, config, tracker, logger) { + constructor(token, config, tracker, logger, credentials) { const mergedConfig = { api_host: "api.mixpanel.com", request_timeout_in_seconds: 10, @@ -32,7 +33,7 @@ class RemoteFeatureFlagsProvider extends FeatureFlagsProvider { request_timeout_in_seconds: mergedConfig.request_timeout_in_seconds, }; - super(providerConfig, "/flags", tracker, "remote", logger); + super(providerConfig, "/flags", tracker, "remote", logger, credentials); } /** diff --git a/packages/mixpanel/lib/flags/utils.js b/packages/mixpanel/lib/flags/utils.js index 2d247b9f..42f2dd24 100644 --- a/packages/mixpanel/lib/flags/utils.js +++ b/packages/mixpanel/lib/flags/utils.js @@ -44,16 +44,26 @@ function normalizedHash(key, salt) { /** * Prepare common query parameters for feature flags API requests - * @param {string} token - Mixpanel project token + * @param {string} token - Mixpanel project token (or null for service account auth) * @param {string} sdkVersion - SDK version string + * @param {string} project_id - Optional project ID for service account authentication * @returns {Object} - Query parameters object */ -function prepareCommonQueryParams(token, sdkVersion) { - return { +function prepareCommonQueryParams(token, sdkVersion, project_id) { + const params = { mp_lib: "node", $lib_version: sdkVersion, - token: token, }; + + if (project_id !== undefined && project_id !== null) { + // Service account authentication - use project_id instead of token + params.project_id = project_id; + } else { + // Token authentication + params.token = token; + } + + return params; } /** diff --git a/packages/mixpanel/lib/mixpanel-node.d.ts b/packages/mixpanel/lib/mixpanel-node.d.ts index 2982f73e..482c04be 100644 --- a/packages/mixpanel/lib/mixpanel-node.d.ts +++ b/packages/mixpanel/lib/mixpanel-node.d.ts @@ -5,6 +5,15 @@ import { LocalFlagsConfig, RemoteFlagsConfig } from "./flags/types"; declare const mixpanel: mixpanel.Mixpanel; declare namespace mixpanel { + export class ServiceAccountCredentials { + constructor(username: string, secret: string, project_id: string); + username: string; + secret: string; + project_id: string; + toHttpBasicAuth(): string; + toString(): string; + } + export type Callback = (err: Error | undefined) => any; export type BatchCallback = (errors: [Error] | undefined) => any; @@ -26,6 +35,7 @@ declare namespace mixpanel { protocol: string; path: string; secret: string; + credentials?: ServiceAccountCredentials; keepAlive: boolean; geolocate: boolean; logger: CustomLogger; diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index d7cdb851..c8d358ab 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -116,17 +116,50 @@ const create_client = function (token, config) { } // add auth params - if (secret) { + const credentials = metrics.config.credentials; + + if (credentials && credentials.username) { + // Service account auth (ONLY for /import endpoint) + if (endpoint === "/import") { + if (request_lib !== https) { + throw new Error("Must use HTTPS with service account credentials"); + } + const encoded = Buffer.from( + credentials.username + ":" + credentials.secret, + ).toString("base64"); + request_options.headers["Authorization"] = "Basic " + encoded; + query_params.project_id = credentials.project_id; + // Do NOT include api_key in POST body when using service accounts + } + // For other endpoints, credentials are ignored (no auth needed) + } else if (secret) { + // Existing API secret logic + if (metrics.config.debug) { + metrics.config.logger.warn( + "api_secret is deprecated and will be removed in a future version. " + + "Please migrate to ServiceAccountCredentials for enhanced security. " + + "See: https://developer.mixpanel.com/docs/service-accounts", + ); + } if (request_lib !== https) { throw new Error("Must use HTTPS if authenticating with API Secret"); } const encoded = Buffer.from(secret + ":").toString("base64"); request_options.headers["Authorization"] = "Basic " + encoded; } else if (key) { + // Existing API key logic + if (metrics.config.debug) { + metrics.config.logger.warn( + "api_key is deprecated and will be removed in a future version. " + + "Please migrate to ServiceAccountCredentials for enhanced security. " + + "See: https://developer.mixpanel.com/docs/service-accounts", + ); + } query_params.api_key = key; } else if (endpoint === "/import") { throw new Error( - "The Mixpanel Client needs a Mixpanel API Secret when importing old events: `init(token, { secret: ... })`", + "The Mixpanel Client needs Service Account credentials or API Secret when importing old events: " + + "`init(token, { credentials: new ServiceAccountCredentials(...) })`", ); } @@ -517,6 +550,7 @@ const create_client = function (token, config) { config.local_flags_config, metrics.track.bind(metrics), config.logger, + config.credentials, ); } @@ -526,6 +560,7 @@ const create_client = function (token, config) { config.remote_flags_config, metrics.track.bind(metrics), config.logger, + config.credentials, ); } @@ -533,6 +568,9 @@ const create_client = function (token, config) { }; // module exporting +const { ServiceAccountCredentials } = require("./credentials"); + module.exports = { init: create_client, + ServiceAccountCredentials: ServiceAccountCredentials, }; diff --git a/packages/mixpanel/readme.md b/packages/mixpanel/readme.md index 2df349bf..59b7b3dd 100644 --- a/packages/mixpanel/readme.md +++ b/packages/mixpanel/readme.md @@ -25,6 +25,16 @@ var Mixpanel = require("mixpanel"); // create an instance of the mixpanel client var mixpanel = Mixpanel.init(""); +// for server-side integrations requiring authentication (e.g., importing old events), +// use service account credentials for enhanced security +const { ServiceAccountCredentials } = Mixpanel; +const credentials = new ServiceAccountCredentials( + "YOUR_SERVICE_ACCOUNT_USERNAME", + "YOUR_SERVICE_ACCOUNT_SECRET", + "YOUR_PROJECT_ID", +); +var mixpanel = Mixpanel.init("", { credentials }); + // initialize mixpanel client configured to communicate over http instead of https var mixpanel = Mixpanel.init("", { protocol: "http", @@ -184,7 +194,40 @@ mixpanel.track_batch([ }, ]); -// import an old event +// Service Account Authentication (Recommended for server-side integrations) +// For enhanced security, use service account credentials instead of API secrets +const { ServiceAccountCredentials } = Mixpanel; + +const credentials = new ServiceAccountCredentials( + "YOUR_SERVICE_ACCOUNT_USERNAME", + "YOUR_SERVICE_ACCOUNT_SECRET", + "YOUR_PROJECT_ID", +); + +const mixpanel_with_sa = Mixpanel.init("YOUR_TOKEN", { credentials }); + +// Import old events (uses service account auth) +mixpanel_with_sa.import("old event", new Date(2012, 4, 20, 12, 34, 56), { + distinct_id: "billybob", + gender: "male", +}); + +// Regular tracking (uses token in payload, no auth header needed) +mixpanel_with_sa.track("test event", { + distinct_id: "billybob", + plan: "premium", +}); + +// Service accounts can also be used with feature flags +const mixpanel_flags = Mixpanel.init("YOUR_TOKEN", { + credentials, + local_flags_config: { + // ... config + }, +}); + +// Legacy: import an old event using API secret (deprecated) +// Service account credentials are recommended instead var mixpanel_importer = Mixpanel.init("valid mixpanel token", { secret: "valid api secret for project", }); diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index fee959a5..d5a459ad 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -325,3 +325,109 @@ describe("import_batch_integration", () => { expect(https.request).toHaveBeenCalledTimes(5); }); }); + +describe("import with service account credentials", () => { + const { ServiceAccountCredentials } = Mixpanel; + let mixpanel; + let credentials; + + beforeEach(() => { + credentials = new ServiceAccountCredentials( + "sa-user", + "sa-secret", + "123456", + ); + mixpanel = Mixpanel.init("token", { credentials }); + vi.spyOn(mixpanel, "send_request"); + + return () => { + mixpanel.send_request.mockRestore(); + }; + }); + + it("validates credentials on construction", () => { + expect( + () => new ServiceAccountCredentials("", "secret", "123"), + ).toThrowError("Service account username cannot be empty"); + expect( + () => new ServiceAccountCredentials("user", "", "123"), + ).toThrowError("Service account secret cannot be empty"); + expect( + () => new ServiceAccountCredentials("user", "secret", ""), + ).toThrowError("Service account project_id cannot be empty"); + expect( + () => new ServiceAccountCredentials(" ", "secret", "123"), + ).toThrowError("Service account username cannot be empty"); + }); + + it("validates credentials types", () => { + expect(() => new ServiceAccountCredentials(123, "secret", "123")).toThrow( + TypeError, + ); + expect(() => new ServiceAccountCredentials("user", 123, "123")).toThrow( + TypeError, + ); + expect(() => new ServiceAccountCredentials("user", "secret", 123)).toThrow( + TypeError, + ); + }); + + it("trims credential values", () => { + const creds = new ServiceAccountCredentials( + " user ", + " secret ", + " 123 ", + ); + expect(creds.username).toBe("user"); + expect(creds.secret).toBe("secret"); + expect(creds.project_id).toBe("123"); + }); + + it("calls send_request with correct endpoint and uses credentials", () => { + const event = "test", + time = mock_now_time, + props = { key1: "val1" }; + + mixpanel.import(event, time, props); + + expect(mixpanel.send_request).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: "/import", + data: expect.objectContaining({ + event: "test", + properties: expect.objectContaining({ + key1: "val1", + token: "token", + time: time, + }), + }), + }), + undefined, + ); + }); + + it("requires HTTPS with service account credentials", () => { + const http_mixpanel = Mixpanel.init("token", { + credentials, + protocol: "http", + }); + + expect(() => { + http_mixpanel.import("test", Date.now(), {}); + }).toThrow("Must use HTTPS with service account credentials"); + }); + + it("toHttpBasicAuth encodes correctly", () => { + const encoded = credentials.toHttpBasicAuth(); + const expected = Buffer.from("sa-user:sa-secret").toString("base64"); + expect(encoded).toBe(expected); + }); + + it("toString masks secret", () => { + const str = credentials.toString(); + expect(str).toContain("sa-user"); + expect(str).toContain("123456"); + expect(str).toContain("***"); + expect(str).not.toContain("sa-secret"); + }); +}); diff --git a/packages/mixpanel/test/send_request.js b/packages/mixpanel/test/send_request.js index 01be8dc7..2c5317f5 100644 --- a/packages/mixpanel/test/send_request.js +++ b/packages/mixpanel/test/send_request.js @@ -293,7 +293,7 @@ describe("send_request", () => { data: { event: `test event` }, }); }).toThrowError( - /The Mixpanel Client needs a Mixpanel API Secret when importing old events/, + /The Mixpanel Client needs Service Account credentials or API Secret when importing old events/, ); }); From 32d813c114fca737750edbb96f8126ac691a223f Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:07:19 +0000 Subject: [PATCH 02/23] Clean up some comments --- packages/mixpanel/lib/flags/flags.js | 9 +++------ packages/mixpanel/lib/mixpanel-node.js | 12 ++++-------- packages/mixpanel/test/import.js | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/mixpanel/lib/flags/flags.js b/packages/mixpanel/lib/flags/flags.js index b7977cd3..929dc187 100644 --- a/packages/mixpanel/lib/flags/flags.js +++ b/packages/mixpanel/lib/flags/flags.js @@ -11,6 +11,7 @@ const { EXPOSURE_EVENT, REQUEST_HEADERS, } = require("./utils"); +const { ServiceAccountCredentials } = require("../credentials"); /** * @typedef {import('./types').SelectedVariant} SelectedVariant @@ -52,18 +53,14 @@ class FeatureFlagsProvider { let commonParams; // Determine auth method - if (this.credentials && this.credentials.username) { + if (this.credentials instanceof ServiceAccountCredentials) { // Service account auth commonParams = prepareCommonQueryParams( null, // No token packageInfo.version, this.credentials.project_id, ); - authHeader = - "Basic " + - Buffer.from( - this.credentials.username + ":" + this.credentials.secret, - ).toString("base64"); + authHeader = "Basic " + this.credentials.toHttpBasicAuth(); } else { // Token auth (existing) commonParams = prepareCommonQueryParams( diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index c8d358ab..4e39f839 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -22,6 +22,7 @@ const { LocalFeatureFlagsProvider, RemoteFeatureFlagsProvider, } = require("./flags"); +const { ServiceAccountCredentials } = require("./credentials"); const DEFAULT_CONFIG = { test: false, @@ -118,18 +119,15 @@ const create_client = function (token, config) { // add auth params const credentials = metrics.config.credentials; - if (credentials && credentials.username) { + if (credentials instanceof ServiceAccountCredentials) { // Service account auth (ONLY for /import endpoint) if (endpoint === "/import") { if (request_lib !== https) { throw new Error("Must use HTTPS with service account credentials"); } - const encoded = Buffer.from( - credentials.username + ":" + credentials.secret, - ).toString("base64"); - request_options.headers["Authorization"] = "Basic " + encoded; + request_options.headers["Authorization"] = + "Basic " + credentials.toHttpBasicAuth(); query_params.project_id = credentials.project_id; - // Do NOT include api_key in POST body when using service accounts } // For other endpoints, credentials are ignored (no auth needed) } else if (secret) { @@ -568,8 +566,6 @@ const create_client = function (token, config) { }; // module exporting -const { ServiceAccountCredentials } = require("./credentials"); - module.exports = { init: create_client, ServiceAccountCredentials: ServiceAccountCredentials, diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index d5a459ad..cbf6cb99 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -430,4 +430,18 @@ describe("import with service account credentials", () => { expect(str).toContain("***"); expect(str).not.toContain("sa-secret"); }); + + it("rejects plain objects that look like credentials", () => { + // Plain object should not be treated as service account credentials + const plain_object_mixpanel = Mixpanel.init("token", { + credentials: { username: "user", secret: "secret", project_id: "123" }, + }); + + // Should fail because plain object is not ServiceAccountCredentials instance + expect(() => { + plain_object_mixpanel.import("test", Date.now(), {}); + }).toThrow( + /The Mixpanel Client needs Service Account credentials or API Secret/, + ); + }); }); From cb0e3c6d6ca985c621a8e3f90115fed83a4dd637 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:15:36 +0000 Subject: [PATCH 03/23] Fix lint and remove debug conditional --- packages/mixpanel/lib/mixpanel-node.js | 24 ++++++++++-------------- packages/mixpanel/test/import.js | 6 +++--- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index 4e39f839..25199ba2 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -132,13 +132,11 @@ const create_client = function (token, config) { // For other endpoints, credentials are ignored (no auth needed) } else if (secret) { // Existing API secret logic - if (metrics.config.debug) { - metrics.config.logger.warn( - "api_secret is deprecated and will be removed in a future version. " + - "Please migrate to ServiceAccountCredentials for enhanced security. " + - "See: https://developer.mixpanel.com/docs/service-accounts", - ); - } + metrics.config.logger.warn( + "api_secret is deprecated and will be removed in a future version. " + + "Please migrate to ServiceAccountCredentials for enhanced security. " + + "See: https://developer.mixpanel.com/docs/service-accounts", + ); if (request_lib !== https) { throw new Error("Must use HTTPS if authenticating with API Secret"); } @@ -146,13 +144,11 @@ const create_client = function (token, config) { request_options.headers["Authorization"] = "Basic " + encoded; } else if (key) { // Existing API key logic - if (metrics.config.debug) { - metrics.config.logger.warn( - "api_key is deprecated and will be removed in a future version. " + - "Please migrate to ServiceAccountCredentials for enhanced security. " + - "See: https://developer.mixpanel.com/docs/service-accounts", - ); - } + metrics.config.logger.warn( + "api_key is deprecated and will be removed in a future version. " + + "Please migrate to ServiceAccountCredentials for enhanced security. " + + "See: https://developer.mixpanel.com/docs/service-accounts", + ); query_params.api_key = key; } else if (endpoint === "/import") { throw new Error( diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index cbf6cb99..1f2a8e7f 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -349,9 +349,9 @@ describe("import with service account credentials", () => { expect( () => new ServiceAccountCredentials("", "secret", "123"), ).toThrowError("Service account username cannot be empty"); - expect( - () => new ServiceAccountCredentials("user", "", "123"), - ).toThrowError("Service account secret cannot be empty"); + expect(() => new ServiceAccountCredentials("user", "", "123")).toThrowError( + "Service account secret cannot be empty", + ); expect( () => new ServiceAccountCredentials("user", "secret", ""), ).toThrowError("Service account project_id cannot be empty"); From b398674c594d68b0692ee1d7c2b3ad6b87c6955a Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:20:27 +0000 Subject: [PATCH 04/23] Fix lint test --- packages/mixpanel/lib/mixpanel-node.d.ts | 2 -- packages/mixpanel/lib/mixpanel-node.js | 4 +++- .../openfeature-server-provider/test/MixpanelProvider.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.d.ts b/packages/mixpanel/lib/mixpanel-node.d.ts index 482c04be..39eafda5 100644 --- a/packages/mixpanel/lib/mixpanel-node.d.ts +++ b/packages/mixpanel/lib/mixpanel-node.d.ts @@ -2,8 +2,6 @@ import LocalFeatureFlagsProvider from "./flags/local_flags"; import RemoteFeatureFlagsProvider from "./flags/remote_flags"; import { LocalFlagsConfig, RemoteFlagsConfig } from "./flags/types"; -declare const mixpanel: mixpanel.Mixpanel; - declare namespace mixpanel { export class ServiceAccountCredentials { constructor(username: string, secret: string, project_id: string); diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index 25199ba2..ca8434f0 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -325,7 +325,9 @@ const create_client = function (token, config) { cb = function (errors, results) { index += 1; if (index === total_request_batches) { - callback && callback(errors, results); + if (callback) { + callback(errors, results); + } } else { send_next_request_batch(index); } diff --git a/packages/openfeature-server-provider/test/MixpanelProvider.test.ts b/packages/openfeature-server-provider/test/MixpanelProvider.test.ts index c8c4cd03..d3007d62 100644 --- a/packages/openfeature-server-provider/test/MixpanelProvider.test.ts +++ b/packages/openfeature-server-provider/test/MixpanelProvider.test.ts @@ -5,7 +5,7 @@ import type { SelectedVariant } from "mixpanel/lib/flags/types"; function createMockFlagsProvider({ flags = new Map(), - ready = true, + ready: _ready = true, } = {}): MixpanelFlagsProvider & { getVariant: ReturnType } { return { getVariant: vi.fn( From 70a023e1943938649eff9ff77bddbdd2b76f9adf Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:48:06 +0000 Subject: [PATCH 05/23] add project id along with token --- packages/mixpanel/lib/flags/utils.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/mixpanel/lib/flags/utils.js b/packages/mixpanel/lib/flags/utils.js index 42f2dd24..fa59bf8f 100644 --- a/packages/mixpanel/lib/flags/utils.js +++ b/packages/mixpanel/lib/flags/utils.js @@ -53,14 +53,12 @@ function prepareCommonQueryParams(token, sdkVersion, project_id) { const params = { mp_lib: "node", $lib_version: sdkVersion, + token: token, }; if (project_id !== undefined && project_id !== null) { - // Service account authentication - use project_id instead of token + // Service account authentication - add project_id params.project_id = project_id; - } else { - // Token authentication - params.token = token; } return params; From 5f17a3685d32cc37e838fee66bfcab48a3caa989 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:39:52 +0000 Subject: [PATCH 06/23] FIx declaration for types --- packages/mixpanel/lib/mixpanel-node.d.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/mixpanel/lib/mixpanel-node.d.ts b/packages/mixpanel/lib/mixpanel-node.d.ts index 39eafda5..68ebf5fa 100644 --- a/packages/mixpanel/lib/mixpanel-node.d.ts +++ b/packages/mixpanel/lib/mixpanel-node.d.ts @@ -3,6 +3,11 @@ import RemoteFeatureFlagsProvider from "./flags/remote_flags"; import { LocalFlagsConfig, RemoteFlagsConfig } from "./flags/types"; declare namespace mixpanel { + export function init( + mixpanelToken: string, + config?: Partial, + ): Mixpanel; + export class ServiceAccountCredentials { constructor(username: string, secret: string, project_id: string); username: string; @@ -413,4 +418,8 @@ declare namespace mixpanel { } } -export = mixpanel; +declare const mixpanelExport: typeof mixpanel.init & { + ServiceAccountCredentials: typeof mixpanel.ServiceAccountCredentials; +}; + +export = mixpanelExport; From 25308a8fd234c25bb0592437e315fead2d9b7a1b Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:00:01 +0000 Subject: [PATCH 07/23] Fix TypeScript export to properly expose init method Export as object with init property instead of function intersection, matching the actual module.exports = { init, ServiceAccountCredentials } --- packages/mixpanel/lib/mixpanel-node.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mixpanel/lib/mixpanel-node.d.ts b/packages/mixpanel/lib/mixpanel-node.d.ts index 68ebf5fa..252ae26e 100644 --- a/packages/mixpanel/lib/mixpanel-node.d.ts +++ b/packages/mixpanel/lib/mixpanel-node.d.ts @@ -418,7 +418,8 @@ declare namespace mixpanel { } } -declare const mixpanelExport: typeof mixpanel.init & { +declare const mixpanelExport: { + init: typeof mixpanel.init; ServiceAccountCredentials: typeof mixpanel.ServiceAccountCredentials; }; From 16d36549f3bdac9b4234a1b5c6c767484ae9d7e4 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:05:01 +0000 Subject: [PATCH 08/23] Revert --- packages/mixpanel/lib/mixpanel-node.d.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.d.ts b/packages/mixpanel/lib/mixpanel-node.d.ts index 252ae26e..edad8036 100644 --- a/packages/mixpanel/lib/mixpanel-node.d.ts +++ b/packages/mixpanel/lib/mixpanel-node.d.ts @@ -418,9 +418,5 @@ declare namespace mixpanel { } } -declare const mixpanelExport: { - init: typeof mixpanel.init; - ServiceAccountCredentials: typeof mixpanel.ServiceAccountCredentials; -}; - -export = mixpanelExport; +// Re-export the namespace types under the default export name +export = mixpanel; From bac8656de25d3eb0c87e6ad3bed3c4bee02937a8 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:56:36 +0000 Subject: [PATCH 09/23] Update documentation --- packages/mixpanel/example-service-account.js | 116 ------------------- packages/mixpanel/example.js | 45 +++++-- packages/mixpanel/lib/mixpanel-node.js | 26 +++-- packages/mixpanel/readme.md | 73 +++++++++++- packages/mixpanel/test/import.js | 2 +- packages/mixpanel/test/send_request.js | 2 +- 6 files changed, 121 insertions(+), 143 deletions(-) delete mode 100644 packages/mixpanel/example-service-account.js diff --git a/packages/mixpanel/example-service-account.js b/packages/mixpanel/example-service-account.js deleted file mode 100644 index 15d48b8c..00000000 --- a/packages/mixpanel/example-service-account.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Example usage of Service Account authentication with mixpanel-node - * - * Service accounts provide enhanced security for server-to-server integrations - * by using unique username/secret pairs instead of shared API secrets. - */ - -const Mixpanel = require("./lib/mixpanel-node"); -const { ServiceAccountCredentials } = Mixpanel; - -// Create service account credentials -// Replace with your actual service account credentials -const credentials = new ServiceAccountCredentials( - "YOUR_SERVICE_ACCOUNT_USERNAME", - "YOUR_SERVICE_ACCOUNT_SECRET", - "YOUR_PROJECT_ID", -); - -// Initialize Mixpanel with service account credentials -const mixpanel = Mixpanel.init("YOUR_PROJECT_TOKEN", { - credentials, - test: true, // Set to false in production - debug: true, // Enable debug logging -}); - -console.log("Service Account Example"); -console.log("=======================\n"); - -// Example 1: Import an old event -console.log("1. Importing an old event..."); -mixpanel.import( - "old_signup", - new Date(2020, 0, 1, 10, 30, 0), - { - distinct_id: "user123", - source: "historical_data", - plan: "premium", - }, - (err) => { - if (err) { - console.error(" Error importing event:", err.message); - } else { - console.log(" ✓ Event imported successfully"); - } - }, -); - -// Example 2: Import multiple events in a batch -console.log("\n2. Importing a batch of events..."); -mixpanel.import_batch( - [ - { - event: "page_view", - properties: { - time: new Date(2020, 0, 15, 14, 0, 0), - distinct_id: "user123", - page: "/home", - }, - }, - { - event: "page_view", - properties: { - time: new Date(2020, 0, 15, 14, 5, 0), - distinct_id: "user123", - page: "/products", - }, - }, - { - event: "purchase", - properties: { - time: new Date(2020, 0, 15, 14, 10, 0), - distinct_id: "user123", - amount: 99.99, - }, - }, - ], - (errors) => { - if (errors) { - console.error(" Errors importing batch:", errors); - } else { - console.log(" ✓ Batch imported successfully"); - } - }, -); - -// Example 3: Regular tracking (uses token in payload, no auth needed) -console.log("\n3. Tracking a current event..."); -mixpanel.track( - "button_clicked", - { - distinct_id: "user456", - button_name: "signup", - page: "/landing", - }, - (err) => { - if (err) { - console.error(" Error tracking event:", err.message); - } else { - console.log(" ✓ Event tracked successfully"); - } - }, -); - -// Example 4: Using with feature flags -console.log("\n4. Service accounts can also be used with feature flags"); -console.log(" (requires local_flags_config or remote_flags_config)"); - -console.log("\nNotes:"); -console.log( - "- Service account auth is only used for /import endpoint and feature flags", -); -console.log( - "- Regular tracking (/track, /engage, /groups) uses token in payload only", -); -console.log("- HTTPS is required when using service account credentials"); -console.log("- This example uses test mode - set test: false in production"); diff --git a/packages/mixpanel/example.js b/packages/mixpanel/example.js index 7d05a7ee..712250bb 100644 --- a/packages/mixpanel/example.js +++ b/packages/mixpanel/example.js @@ -82,22 +82,33 @@ mixpanel.track("test", function (err) { } }); -// import an old event -const mixpanel_importer = Mixpanel.init("valid mixpanel token", { - secret: "valid api secret for project", -}); -mixpanel_importer.set_config({ debug: true }); +// ============================================================================ +// Service Account Authentication (RECOMMENDED for importing old events) +// ============================================================================ +// Service accounts provide enhanced security for importing historical events +// Learn more: https://developer.mixpanel.com/reference/service-accounts-api + +const { ServiceAccountCredentials } = Mixpanel; + +const credentials = new ServiceAccountCredentials( + "YOUR_SERVICE_ACCOUNT_USERNAME", // From Mixpanel project settings + "YOUR_SERVICE_ACCOUNT_SECRET", // From Mixpanel project settings + "YOUR_PROJECT_ID", // Your Mixpanel project ID +); -// needs to be in the system once for it to show up in the interface -mixpanel_importer.track("old event", { gender: "" }); +const mixpanel_with_sa = Mixpanel.init("valid mixpanel token", { + credentials, +}); +mixpanel_with_sa.set_config({ debug: true }); -mixpanel_importer.import("old event", new Date(2012, 4, 20, 12, 34, 56), { +// Import old events (uses service account auth) +mixpanel_with_sa.import("old event", new Date(2012, 4, 20, 12, 34, 56), { distinct_id: "billybob", gender: "male", }); -// import multiple events at once -mixpanel_importer.import_batch([ +// Import multiple events at once +mixpanel_with_sa.import_batch([ { event: "old event", properties: { @@ -115,3 +126,17 @@ mixpanel_importer.import_batch([ }, }, ]); + +// ============================================================================ +// DEPRECATED: API Secret (use Service Accounts instead) +// ============================================================================ +// This method still works but will be removed in a future version +const mixpanel_importer = Mixpanel.init("valid mixpanel token", { + secret: "valid api secret for project", // DEPRECATED - use ServiceAccountCredentials +}); +mixpanel_importer.set_config({ debug: true }); + +mixpanel_importer.import("old event", new Date(2012, 4, 20, 12, 34, 56), { + distinct_id: "billybob", + gender: "male", +}); diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index ca8434f0..b0cc59ff 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -131,11 +131,11 @@ const create_client = function (token, config) { } // For other endpoints, credentials are ignored (no auth needed) } else if (secret) { - // Existing API secret logic + // DEPRECATED: API secret auth (use ServiceAccountCredentials instead) metrics.config.logger.warn( - "api_secret is deprecated and will be removed in a future version. " + - "Please migrate to ServiceAccountCredentials for enhanced security. " + - "See: https://developer.mixpanel.com/docs/service-accounts", + "⚠️ DEPRECATION WARNING: api_secret is deprecated and will be removed in a future version.\n" + + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + + " See: https://developer.mixpanel.com/reference/service-accounts-api", ); if (request_lib !== https) { throw new Error("Must use HTTPS if authenticating with API Secret"); @@ -143,17 +143,23 @@ const create_client = function (token, config) { const encoded = Buffer.from(secret + ":").toString("base64"); request_options.headers["Authorization"] = "Basic " + encoded; } else if (key) { - // Existing API key logic + // DEPRECATED: API key auth (use ServiceAccountCredentials instead) metrics.config.logger.warn( - "api_key is deprecated and will be removed in a future version. " + - "Please migrate to ServiceAccountCredentials for enhanced security. " + - "See: https://developer.mixpanel.com/docs/service-accounts", + "⚠️ DEPRECATION WARNING: api_key is deprecated and will be removed in a future version.\n" + + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + + " See: https://developer.mixpanel.com/reference/service-accounts-api", ); query_params.api_key = key; } else if (endpoint === "/import") { throw new Error( - "The Mixpanel Client needs Service Account credentials or API Secret when importing old events: " + - "`init(token, { credentials: new ServiceAccountCredentials(...) })`", + "The /import endpoint requires authentication.\n\n" + + "RECOMMENDED: Use service account credentials (preferred method):\n" + + " const { ServiceAccountCredentials } = require('mixpanel');\n" + + " const credentials = new ServiceAccountCredentials('username', 'secret', 'project_id');\n" + + " const mixpanel = Mixpanel.init('token', { credentials });\n\n" + + "Learn more: https://developer.mixpanel.com/reference/service-accounts-api\n\n" + + "DEPRECATED: API secret (will be removed in a future version):\n" + + " const mixpanel = Mixpanel.init('token', { secret: 'your-api-secret' });", ); } diff --git a/packages/mixpanel/readme.md b/packages/mixpanel/readme.md index 59b7b3dd..b8c15914 100644 --- a/packages/mixpanel/readme.md +++ b/packages/mixpanel/readme.md @@ -194,8 +194,12 @@ mixpanel.track_batch([ }, ]); -// Service Account Authentication (Recommended for server-side integrations) -// For enhanced security, use service account credentials instead of API secrets +// ============================================================================ +// Service Account Authentication (RECOMMENDED - Preferred Method) +// ============================================================================ +// Service accounts provide enhanced security for server-to-server integrations. +// This is the PREFERRED authentication method. API secrets are deprecated. +// Learn more: https://developer.mixpanel.com/reference/service-accounts-api const { ServiceAccountCredentials } = Mixpanel; const credentials = new ServiceAccountCredentials( @@ -226,10 +230,13 @@ const mixpanel_flags = Mixpanel.init("YOUR_TOKEN", { }, }); -// Legacy: import an old event using API secret (deprecated) -// Service account credentials are recommended instead +// ============================================================================ +// DEPRECATED: API Secret Authentication (Legacy - Not Recommended) +// ============================================================================ +// ⚠️ WARNING: API secrets are DEPRECATED and will be removed in a future version. +// Please migrate to Service Account credentials above for enhanced security. var mixpanel_importer = Mixpanel.init("valid mixpanel token", { - secret: "valid api secret for project", + secret: "valid api secret for project", // DEPRECATED - use ServiceAccountCredentials instead }); // needs to be in the system once for it to show up in the interface @@ -261,6 +268,62 @@ mixpanel_importer.import_batch([ ]); ``` +## Service Account Authentication + +**Documentation:** https://developer.mixpanel.com/reference/service-accounts-api + +### When to Use Service Accounts + +Service account credentials are **required** for: +- **Importing historical events** - Events older than 5 days via `/import` endpoint +- **Server-side feature flags** - Evaluating feature flags with authentication + +Service accounts are **NOT needed** for: +- **Regular event tracking** - `track()` uses project token only +- **People analytics** - `people.set()`, `people.increment()`, etc. +- **Group analytics** - `groups.set()`, etc. + +### Migrating from API Secrets (Deprecated) + +If you're currently using API secrets (deprecated), here's how to upgrade: + +**Before (deprecated):** +```javascript +// Old method - still works but deprecated +var mixpanel = Mixpanel.init('TOKEN', { + secret: 'your-api-secret' +}); + +mixpanel.import('event', new Date(2020, 1, 1), { + distinct_id: 'user123' +}); +``` + +**After (recommended):** +```javascript +// New method - enhanced security with service accounts +const { ServiceAccountCredentials } = Mixpanel; + +const credentials = new ServiceAccountCredentials( + 'service-account-username', // From Mixpanel project settings + 'service-account-secret', // From Mixpanel project settings + 'project-id' // Your Mixpanel project ID +); + +var mixpanel = Mixpanel.init('TOKEN', { credentials }); + +mixpanel.import('event', new Date(2020, 1, 1), { + distinct_id: 'user123' +}); +``` + +**Key differences:** +- Service accounts use username/secret pairs instead of a single shared secret +- Project ID is explicitly specified for better security +- Each service account can have specific permissions +- Easier to rotate credentials without affecting other integrations +- **Learn more:** https://developer.mixpanel.com/reference/service-accounts-api + ## FAQ **Where is `mixpanel.identify()`?** diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index 1f2a8e7f..966682b2 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -441,7 +441,7 @@ describe("import with service account credentials", () => { expect(() => { plain_object_mixpanel.import("test", Date.now(), {}); }).toThrow( - /The Mixpanel Client needs Service Account credentials or API Secret/, + /The \/import endpoint requires authentication/, ); }); }); diff --git a/packages/mixpanel/test/send_request.js b/packages/mixpanel/test/send_request.js index 2c5317f5..15dde399 100644 --- a/packages/mixpanel/test/send_request.js +++ b/packages/mixpanel/test/send_request.js @@ -293,7 +293,7 @@ describe("send_request", () => { data: { event: `test event` }, }); }).toThrowError( - /The Mixpanel Client needs Service Account credentials or API Secret when importing old events/, + /The \/import endpoint requires authentication/, ); }); From 2bcc48f31f7bb5895c77a4804cd216e73009dc78 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:13:25 +0000 Subject: [PATCH 10/23] Add more tests --- packages/mixpanel/lib/credentials.js | 6 +- packages/mixpanel/test/flags/local_flags.js | 32 +++++++ packages/mixpanel/test/import.js | 85 +++++++++++++++++-- .../test/MixpanelProvider.test.ts | 1 - 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/packages/mixpanel/lib/credentials.js b/packages/mixpanel/lib/credentials.js index c142ac5e..aa9b1200 100644 --- a/packages/mixpanel/lib/credentials.js +++ b/packages/mixpanel/lib/credentials.js @@ -41,13 +41,13 @@ class ServiceAccountCredentials { const trimmedProjectId = project_id.trim(); if (!trimmedUsername) { - throw new Error("Service account username cannot be empty"); + throw new TypeError("Service account username cannot be empty"); } if (!trimmedSecret) { - throw new Error("Service account secret cannot be empty"); + throw new TypeError("Service account secret cannot be empty"); } if (!trimmedProjectId) { - throw new Error("Service account project_id cannot be empty"); + throw new TypeError("Service account project_id cannot be empty"); } this.username = trimmedUsername; diff --git a/packages/mixpanel/test/flags/local_flags.js b/packages/mixpanel/test/flags/local_flags.js index 2608663e..54949104 100644 --- a/packages/mixpanel/test/flags/local_flags.js +++ b/packages/mixpanel/test/flags/local_flags.js @@ -848,4 +848,36 @@ describe("LocalFeatureFlagsProvider", () => { expect(result).toBe(false); }); }); + + describe("with service account credentials", () => { + it("should use service account auth when credentials provided", async () => { + const { ServiceAccountCredentials } = require("../../lib/credentials"); + const credentials = new ServiceAccountCredentials( + "sa-user", + "sa-secret", + "project-123", + ); + + const flagsConfig = { test_flag: "control" }; + mockFlagDefinitionsResponse([ + createTestFlag({ flagKey: "test_flag", variants: flagsConfig }), + ]); + + const tracker = () => {}; + const logger = { debug: () => {}, info: () => {}, warn: () => {} }; + + const provider = new LocalFeatureFlagsProvider( + "test-token", + { api_host: "localhost" }, + tracker, + logger, + credentials, + ); + + await provider.areFlagsReady(); + + // Verify the provider was created with credentials + expect(provider.credentials).toBe(credentials); + }); + }); }); diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index 966682b2..99c1942c 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -348,16 +348,16 @@ describe("import with service account credentials", () => { it("validates credentials on construction", () => { expect( () => new ServiceAccountCredentials("", "secret", "123"), - ).toThrowError("Service account username cannot be empty"); - expect(() => new ServiceAccountCredentials("user", "", "123")).toThrowError( - "Service account secret cannot be empty", + ).toThrow(TypeError); + expect(() => new ServiceAccountCredentials("user", "", "123")).toThrow( + TypeError, ); expect( () => new ServiceAccountCredentials("user", "secret", ""), - ).toThrowError("Service account project_id cannot be empty"); + ).toThrow(TypeError); expect( () => new ServiceAccountCredentials(" ", "secret", "123"), - ).toThrowError("Service account username cannot be empty"); + ).toThrow(TypeError); }); it("validates credentials types", () => { @@ -444,4 +444,79 @@ describe("import with service account credentials", () => { /The \/import endpoint requires authentication/, ); }); + + it("does not send auth headers for non-import endpoints", () => { + const mixpanel_sa = Mixpanel.init("token", { credentials }); + vi.spyOn(mixpanel_sa, "send_request"); + + // Track should not use service account credentials + mixpanel_sa.track("test event", { distinct_id: "user123" }); + + const call_args = mixpanel_sa.send_request.mock.calls[0][0]; + expect(call_args.endpoint).not.toBe("/import"); + }); +}); + +describe("deprecation warnings", () => { + let mixpanel; + let mockLogger; + + beforeEach(() => { + mockLogger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + }); + + it("warns when using api_secret", () => { + mixpanel = Mixpanel.init("token", { + secret: "api-secret", + logger: mockLogger, + }); + + mixpanel.import("test", Date.now(), { distinct_id: "user" }); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("DEPRECATION WARNING"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("api_secret is deprecated"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("ServiceAccountCredentials"), + ); + }); + + it("warns when using api_key", () => { + mixpanel = Mixpanel.init("token", { + key: "api-key", + logger: mockLogger, + }); + + mixpanel.track("test", { distinct_id: "user" }); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("DEPRECATION WARNING"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("api_key is deprecated"), + ); + }); + + it("does not warn when using service account credentials", () => { + const { ServiceAccountCredentials } = Mixpanel; + const creds = new ServiceAccountCredentials("user", "secret", "123"); + + mixpanel = Mixpanel.init("token", { + credentials: creds, + logger: mockLogger, + }); + + mixpanel.import("test", Date.now(), { distinct_id: "user" }); + + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); }); diff --git a/packages/openfeature-server-provider/test/MixpanelProvider.test.ts b/packages/openfeature-server-provider/test/MixpanelProvider.test.ts index d3007d62..e02b76c7 100644 --- a/packages/openfeature-server-provider/test/MixpanelProvider.test.ts +++ b/packages/openfeature-server-provider/test/MixpanelProvider.test.ts @@ -5,7 +5,6 @@ import type { SelectedVariant } from "mixpanel/lib/flags/types"; function createMockFlagsProvider({ flags = new Map(), - ready: _ready = true, } = {}): MixpanelFlagsProvider & { getVariant: ReturnType } { return { getVariant: vi.fn( From a4d41e03c93ab92cb42ba487a785ab9c09506661 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:17:19 +0000 Subject: [PATCH 11/23] Remove comments --- packages/mixpanel/lib/flags/flags.js | 5 +---- packages/mixpanel/lib/flags/utils.js | 2 +- packages/mixpanel/lib/mixpanel-node.js | 6 ++---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/mixpanel/lib/flags/flags.js b/packages/mixpanel/lib/flags/flags.js index 929dc187..46cd72ab 100644 --- a/packages/mixpanel/lib/flags/flags.js +++ b/packages/mixpanel/lib/flags/flags.js @@ -52,17 +52,14 @@ class FeatureFlagsProvider { let authHeader; let commonParams; - // Determine auth method if (this.credentials instanceof ServiceAccountCredentials) { - // Service account auth commonParams = prepareCommonQueryParams( - null, // No token + this.providerConfig.token, packageInfo.version, this.credentials.project_id, ); authHeader = "Basic " + this.credentials.toHttpBasicAuth(); } else { - // Token auth (existing) commonParams = prepareCommonQueryParams( this.providerConfig.token, packageInfo.version, diff --git a/packages/mixpanel/lib/flags/utils.js b/packages/mixpanel/lib/flags/utils.js index fa59bf8f..073e39b6 100644 --- a/packages/mixpanel/lib/flags/utils.js +++ b/packages/mixpanel/lib/flags/utils.js @@ -44,7 +44,7 @@ function normalizedHash(key, salt) { /** * Prepare common query parameters for feature flags API requests - * @param {string} token - Mixpanel project token (or null for service account auth) + * @param {string} token - Mixpanel project token * @param {string} sdkVersion - SDK version string * @param {string} project_id - Optional project ID for service account authentication * @returns {Object} - Query parameters object diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index b0cc59ff..4e207bce 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -120,7 +120,6 @@ const create_client = function (token, config) { const credentials = metrics.config.credentials; if (credentials instanceof ServiceAccountCredentials) { - // Service account auth (ONLY for /import endpoint) if (endpoint === "/import") { if (request_lib !== https) { throw new Error("Must use HTTPS with service account credentials"); @@ -129,11 +128,10 @@ const create_client = function (token, config) { "Basic " + credentials.toHttpBasicAuth(); query_params.project_id = credentials.project_id; } - // For other endpoints, credentials are ignored (no auth needed) } else if (secret) { // DEPRECATED: API secret auth (use ServiceAccountCredentials instead) metrics.config.logger.warn( - "⚠️ DEPRECATION WARNING: api_secret is deprecated and will be removed in a future version.\n" + + "DEPRECATION WARNING: api_secret is deprecated and will be removed in a future version.\n" + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + " See: https://developer.mixpanel.com/reference/service-accounts-api", ); @@ -145,7 +143,7 @@ const create_client = function (token, config) { } else if (key) { // DEPRECATED: API key auth (use ServiceAccountCredentials instead) metrics.config.logger.warn( - "⚠️ DEPRECATION WARNING: api_key is deprecated and will be removed in a future version.\n" + + "DEPRECATION WARNING: api_key is deprecated and will be removed in a future version.\n" + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + " See: https://developer.mixpanel.com/reference/service-accounts-api", ); From 538b77c439c2c393143d8fdb45f0d01da54cf3d5 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:22:32 +0000 Subject: [PATCH 12/23] Fix lint --- packages/mixpanel/lib/mixpanel-node.d.ts | 1 - packages/mixpanel/readme.md | 25 ++++++++++++++---------- packages/mixpanel/test/import.js | 22 ++++++++++----------- packages/mixpanel/test/send_request.js | 4 +--- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.d.ts b/packages/mixpanel/lib/mixpanel-node.d.ts index edad8036..e7e077cb 100644 --- a/packages/mixpanel/lib/mixpanel-node.d.ts +++ b/packages/mixpanel/lib/mixpanel-node.d.ts @@ -418,5 +418,4 @@ declare namespace mixpanel { } } -// Re-export the namespace types under the default export name export = mixpanel; diff --git a/packages/mixpanel/readme.md b/packages/mixpanel/readme.md index b8c15914..cc213a48 100644 --- a/packages/mixpanel/readme.md +++ b/packages/mixpanel/readme.md @@ -275,10 +275,12 @@ mixpanel_importer.import_batch([ ### When to Use Service Accounts Service account credentials are **required** for: + - **Importing historical events** - Events older than 5 days via `/import` endpoint - **Server-side feature flags** - Evaluating feature flags with authentication Service accounts are **NOT needed** for: + - **Regular event tracking** - `track()` uses project token only - **People analytics** - `people.set()`, `people.increment()`, etc. - **Group analytics** - `groups.set()`, etc. @@ -288,36 +290,39 @@ Service accounts are **NOT needed** for: If you're currently using API secrets (deprecated), here's how to upgrade: **Before (deprecated):** + ```javascript // Old method - still works but deprecated -var mixpanel = Mixpanel.init('TOKEN', { - secret: 'your-api-secret' +var mixpanel = Mixpanel.init("TOKEN", { + secret: "your-api-secret", }); -mixpanel.import('event', new Date(2020, 1, 1), { - distinct_id: 'user123' +mixpanel.import("event", new Date(2020, 1, 1), { + distinct_id: "user123", }); ``` **After (recommended):** + ```javascript // New method - enhanced security with service accounts const { ServiceAccountCredentials } = Mixpanel; const credentials = new ServiceAccountCredentials( - 'service-account-username', // From Mixpanel project settings - 'service-account-secret', // From Mixpanel project settings - 'project-id' // Your Mixpanel project ID + "service-account-username", // From Mixpanel project settings + "service-account-secret", // From Mixpanel project settings + "project-id", // Your Mixpanel project ID ); -var mixpanel = Mixpanel.init('TOKEN', { credentials }); +var mixpanel = Mixpanel.init("TOKEN", { credentials }); -mixpanel.import('event', new Date(2020, 1, 1), { - distinct_id: 'user123' +mixpanel.import("event", new Date(2020, 1, 1), { + distinct_id: "user123", }); ``` **Key differences:** + - Service accounts use username/secret pairs instead of a single shared secret - Project ID is explicitly specified for better security - Each service account can have specific permissions diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index 99c1942c..2bb4a00f 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -346,18 +346,18 @@ describe("import with service account credentials", () => { }); it("validates credentials on construction", () => { - expect( - () => new ServiceAccountCredentials("", "secret", "123"), - ).toThrow(TypeError); + expect(() => new ServiceAccountCredentials("", "secret", "123")).toThrow( + TypeError, + ); expect(() => new ServiceAccountCredentials("user", "", "123")).toThrow( TypeError, ); - expect( - () => new ServiceAccountCredentials("user", "secret", ""), - ).toThrow(TypeError); - expect( - () => new ServiceAccountCredentials(" ", "secret", "123"), - ).toThrow(TypeError); + expect(() => new ServiceAccountCredentials("user", "secret", "")).toThrow( + TypeError, + ); + expect(() => new ServiceAccountCredentials(" ", "secret", "123")).toThrow( + TypeError, + ); }); it("validates credentials types", () => { @@ -440,9 +440,7 @@ describe("import with service account credentials", () => { // Should fail because plain object is not ServiceAccountCredentials instance expect(() => { plain_object_mixpanel.import("test", Date.now(), {}); - }).toThrow( - /The \/import endpoint requires authentication/, - ); + }).toThrow(/The \/import endpoint requires authentication/); }); it("does not send auth headers for non-import endpoints", () => { diff --git a/packages/mixpanel/test/send_request.js b/packages/mixpanel/test/send_request.js index 15dde399..ec10e79f 100644 --- a/packages/mixpanel/test/send_request.js +++ b/packages/mixpanel/test/send_request.js @@ -292,9 +292,7 @@ describe("send_request", () => { endpoint: `/import`, data: { event: `test event` }, }); - }).toThrowError( - /The \/import endpoint requires authentication/, - ); + }).toThrowError(/The \/import endpoint requires authentication/); }); it("sets basic auth header if API secret is provided", () => { From 4a4c0784bc5e3530289a326ade556ab35a099df3 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:18:02 +0000 Subject: [PATCH 13/23] Only warn deprecation on client creation --- packages/mixpanel/lib/mixpanel-node.js | 28 +++++++++++++++----------- packages/mixpanel/test/import.js | 16 +++++++++++---- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index 4e207bce..9282d332 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -129,24 +129,12 @@ const create_client = function (token, config) { query_params.project_id = credentials.project_id; } } else if (secret) { - // DEPRECATED: API secret auth (use ServiceAccountCredentials instead) - metrics.config.logger.warn( - "DEPRECATION WARNING: api_secret is deprecated and will be removed in a future version.\n" + - " Please migrate to ServiceAccountCredentials for enhanced security.\n" + - " See: https://developer.mixpanel.com/reference/service-accounts-api", - ); if (request_lib !== https) { throw new Error("Must use HTTPS if authenticating with API Secret"); } const encoded = Buffer.from(secret + ":").toString("base64"); request_options.headers["Authorization"] = "Basic " + encoded; } else if (key) { - // DEPRECATED: API key auth (use ServiceAccountCredentials instead) - metrics.config.logger.warn( - "DEPRECATION WARNING: api_key is deprecated and will be removed in a future version.\n" + - " Please migrate to ServiceAccountCredentials for enhanced security.\n" + - " See: https://developer.mixpanel.com/reference/service-accounts-api", - ); query_params.api_key = key; } else if (endpoint === "/import") { throw new Error( @@ -537,6 +525,22 @@ const create_client = function (token, config) { metrics.config.port = Number(port); } } + + // Emit deprecation warnings for old auth methods at client creation + if (config && config.secret) { + metrics.config.logger.warn( + "DEPRECATION WARNING: api_secret is deprecated and will be removed in a future version.\n" + + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + + " See: https://developer.mixpanel.com/reference/service-accounts-api", + ); + } + if (config && config.key) { + metrics.config.logger.warn( + "DEPRECATION WARNING: api_key is deprecated and will be removed in a future version.\n" + + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + + " See: https://developer.mixpanel.com/reference/service-accounts-api", + ); + } }; if (config) { diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index 2bb4a00f..640f95f2 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -475,8 +475,7 @@ describe("deprecation warnings", () => { logger: mockLogger, }); - mixpanel.import("test", Date.now(), { distinct_id: "user" }); - + // Warning should be emitted at init time, not during import expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining("DEPRECATION WARNING"), ); @@ -486,6 +485,11 @@ describe("deprecation warnings", () => { expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining("ServiceAccountCredentials"), ); + + // Reset mock to verify it doesn't warn again on subsequent requests + mockLogger.warn.mockClear(); + mixpanel.import("test", Date.now(), { distinct_id: "user" }); + expect(mockLogger.warn).not.toHaveBeenCalled(); }); it("warns when using api_key", () => { @@ -494,14 +498,18 @@ describe("deprecation warnings", () => { logger: mockLogger, }); - mixpanel.track("test", { distinct_id: "user" }); - + // Warning should be emitted at init time, not during track expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining("DEPRECATION WARNING"), ); expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringContaining("api_key is deprecated"), ); + + // Reset mock to verify it doesn't warn again on subsequent requests + mockLogger.warn.mockClear(); + mixpanel.track("test", { distinct_id: "user" }); + expect(mockLogger.warn).not.toHaveBeenCalled(); }); it("does not warn when using service account credentials", () => { From 279889e31d11ab49a0f74c77a1c8147f3c97cfd7 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:15:47 +0000 Subject: [PATCH 14/23] Clean up nits from copilot --- packages/mixpanel/lib/credentials.js | 3 +-- packages/mixpanel/lib/mixpanel-node.js | 3 ++- packages/mixpanel/test/flags/local_flags.js | 6 ++++++ packages/mixpanel/test/import.js | 6 +++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/mixpanel/lib/credentials.js b/packages/mixpanel/lib/credentials.js index aa9b1200..570c0211 100644 --- a/packages/mixpanel/lib/credentials.js +++ b/packages/mixpanel/lib/credentials.js @@ -22,8 +22,7 @@ class ServiceAccountCredentials { * @param {string} username - Service account username * @param {string} secret - Service account secret * @param {string} project_id - Mixpanel project ID - * @throws {TypeError} If any parameter is not a string - * @throws {ValueError} If any parameter is empty or whitespace-only + * @throws {TypeError} If any parameter is not a string, empty, or whitespace-only */ constructor(username, secret, project_id) { if (typeof username !== "string") { diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index 9282d332..eb34e386 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -140,7 +140,8 @@ const create_client = function (token, config) { throw new Error( "The /import endpoint requires authentication.\n\n" + "RECOMMENDED: Use service account credentials (preferred method):\n" + - " const { ServiceAccountCredentials } = require('mixpanel');\n" + + " const Mixpanel = require('mixpanel');\n" + + " const { ServiceAccountCredentials } = Mixpanel;\n" + " const credentials = new ServiceAccountCredentials('username', 'secret', 'project_id');\n" + " const mixpanel = Mixpanel.init('token', { credentials });\n\n" + "Learn more: https://developer.mixpanel.com/reference/service-accounts-api\n\n" + diff --git a/packages/mixpanel/test/flags/local_flags.js b/packages/mixpanel/test/flags/local_flags.js index 54949104..2f06a8bd 100644 --- a/packages/mixpanel/test/flags/local_flags.js +++ b/packages/mixpanel/test/flags/local_flags.js @@ -878,6 +878,12 @@ describe("LocalFeatureFlagsProvider", () => { // Verify the provider was created with credentials expect(provider.credentials).toBe(credentials); + + // Note: The service account credentials (username, secret, project_id) + // are passed to the FeatureFlagsProvider parent class which uses them + // to construct the Authorization header (Basic auth with username:secret) + // and includes project_id as a query parameter when calling the flags endpoint. + // This behavior is tested in the FeatureFlagsProvider base class tests. }); }); }); diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index 640f95f2..c70fe39d 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -339,10 +339,10 @@ describe("import with service account credentials", () => { ); mixpanel = Mixpanel.init("token", { credentials }); vi.spyOn(mixpanel, "send_request"); + }); - return () => { - mixpanel.send_request.mockRestore(); - }; + afterEach(() => { + mixpanel.send_request.mockRestore(); }); it("validates credentials on construction", () => { From eb1bf5cbba4b2fb88ae4ed4233b8a8277c6715d0 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:30:16 +0000 Subject: [PATCH 15/23] Fix signatures --- packages/mixpanel/lib/credentials.js | 7 ++++++- packages/mixpanel/lib/flags/flags.d.ts | 13 ++++++++++--- packages/mixpanel/lib/flags/local_flags.d.ts | 3 ++- packages/mixpanel/lib/flags/remote_flags.d.ts | 5 +++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/mixpanel/lib/credentials.js b/packages/mixpanel/lib/credentials.js index 570c0211..13ecc528 100644 --- a/packages/mixpanel/lib/credentials.js +++ b/packages/mixpanel/lib/credentials.js @@ -52,6 +52,11 @@ class ServiceAccountCredentials { this.username = trimmedUsername; this.secret = trimmedSecret; this.project_id = trimmedProjectId; + + // Cache the base64-encoded auth to avoid recomputing on every request + this._cachedAuth = Buffer.from( + this.username + ":" + this.secret, + ).toString("base64"); } /** @@ -60,7 +65,7 @@ class ServiceAccountCredentials { * @returns {string} Base64-encoded username:secret for Authorization header */ toHttpBasicAuth() { - return Buffer.from(this.username + ":" + this.secret).toString("base64"); + return this._cachedAuth; } /** diff --git a/packages/mixpanel/lib/flags/flags.d.ts b/packages/mixpanel/lib/flags/flags.d.ts index 9ac591d1..02e43037 100644 --- a/packages/mixpanel/lib/flags/flags.d.ts +++ b/packages/mixpanel/lib/flags/flags.d.ts @@ -2,7 +2,7 @@ * TypeScript type definitions for Base Feature Flags Provider */ -import { CustomLogger } from "../mixpanel-node"; +import { CustomLogger, ServiceAccountCredentials } from "../mixpanel-node"; import { SelectedVariant, FlagContext } from "./types"; /** @@ -22,16 +22,23 @@ export class FeatureFlagsProvider { providerConfig: FeatureFlagsConfig; endpoint: string; logger: CustomLogger | null; + credentials?: ServiceAccountCredentials; /** - * @param config - Common configuration for feature flag providers + * @param providerConfig - Common configuration for feature flag providers * @param endpoint - API endpoint path (i.e., '/flags' or '/flags/definitions') + * @param tracker - Function to track events + * @param evaluationMode - The feature flag evaluation mode * @param logger - Logger instance + * @param credentials - Optional service account credentials */ constructor( - config: FeatureFlagsConfig, + providerConfig: FeatureFlagsConfig, endpoint: string, + tracker: Function, + evaluationMode: string, logger: CustomLogger | null, + credentials?: ServiceAccountCredentials, ); /** diff --git a/packages/mixpanel/lib/flags/local_flags.d.ts b/packages/mixpanel/lib/flags/local_flags.d.ts index 25c5f0a6..d28b0e1d 100644 --- a/packages/mixpanel/lib/flags/local_flags.d.ts +++ b/packages/mixpanel/lib/flags/local_flags.d.ts @@ -3,7 +3,7 @@ */ import { LocalFlagsConfig, FlagContext, SelectedVariant } from "./types"; -import { CustomLogger } from "../mixpanel-node"; +import { CustomLogger, ServiceAccountCredentials } from "../mixpanel-node"; /** * Local Feature Flags Provider @@ -20,6 +20,7 @@ export default class LocalFeatureFlagsProvider { callback: (err?: Error) => void, ) => void, logger: CustomLogger, + credentials?: ServiceAccountCredentials, ); /** diff --git a/packages/mixpanel/lib/flags/remote_flags.d.ts b/packages/mixpanel/lib/flags/remote_flags.d.ts index f857913c..b1cd1bbc 100644 --- a/packages/mixpanel/lib/flags/remote_flags.d.ts +++ b/packages/mixpanel/lib/flags/remote_flags.d.ts @@ -2,7 +2,7 @@ * TypeScript definitions for Remote Feature Flags Provider */ -import { CustomLogger } from "../mixpanel-node"; +import { CustomLogger, ServiceAccountCredentials } from "../mixpanel-node"; import { RemoteFlagsConfig, FlagContext, SelectedVariant } from "./types"; /** @@ -13,13 +13,14 @@ export default class RemoteFeatureFlagsProvider { constructor( token: string, config: RemoteFlagsConfig, - logger: CustomLogger, tracker: ( distinct_id: string, event: string, properties: object, callback: (err?: Error) => void, ) => void, + logger: CustomLogger, + credentials?: ServiceAccountCredentials, ); /** From dce332b02245549b4f78a51fa472c096edfb2083 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:34:41 +0000 Subject: [PATCH 16/23] Run lint --- packages/mixpanel/lib/credentials.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mixpanel/lib/credentials.js b/packages/mixpanel/lib/credentials.js index 13ecc528..1f45016e 100644 --- a/packages/mixpanel/lib/credentials.js +++ b/packages/mixpanel/lib/credentials.js @@ -54,9 +54,9 @@ class ServiceAccountCredentials { this.project_id = trimmedProjectId; // Cache the base64-encoded auth to avoid recomputing on every request - this._cachedAuth = Buffer.from( - this.username + ":" + this.secret, - ).toString("base64"); + this._cachedAuth = Buffer.from(this.username + ":" + this.secret).toString( + "base64", + ); } /** From 0e591a6029413bff2984e91b4c5557d72d8c18eb Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:55:34 +0000 Subject: [PATCH 17/23] Revert test change --- .../openfeature-server-provider/test/MixpanelProvider.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/openfeature-server-provider/test/MixpanelProvider.test.ts b/packages/openfeature-server-provider/test/MixpanelProvider.test.ts index e02b76c7..c8c4cd03 100644 --- a/packages/openfeature-server-provider/test/MixpanelProvider.test.ts +++ b/packages/openfeature-server-provider/test/MixpanelProvider.test.ts @@ -5,6 +5,7 @@ import type { SelectedVariant } from "mixpanel/lib/flags/types"; function createMockFlagsProvider({ flags = new Map(), + ready = true, } = {}): MixpanelFlagsProvider & { getVariant: ReturnType } { return { getVariant: vi.fn( From 8c7e526efae6f942dee65e4d6d8ce48140caef66 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:05:52 +0000 Subject: [PATCH 18/23] Add more tests --- packages/mixpanel/test/flags/remote_flags.js | 109 +++++++++++++++++++ packages/mixpanel/test/send_request.js | 26 +++++ 2 files changed, 135 insertions(+) diff --git a/packages/mixpanel/test/flags/remote_flags.js b/packages/mixpanel/test/flags/remote_flags.js index 0f0d763a..dd4e2984 100644 --- a/packages/mixpanel/test/flags/remote_flags.js +++ b/packages/mixpanel/test/flags/remote_flags.js @@ -486,4 +486,113 @@ describe("RemoteFeatureFlagProvider", () => { expect(result).toBe(false); }); }); + + describe("Service Account Credentials", () => { + const { ServiceAccountCredentials } = require("../../lib/credentials"); + let saProvider; + let credentials; + + beforeEach(() => { + credentials = new ServiceAccountCredentials( + "sa-username", + "sa-secret", + "test-project-123", + ); + + let mockLogger = { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + }; + + let config = { + api_host: flagsEndpointHostName, + }; + + mockTracker = vi.fn(); + + saProvider = new RemoteFeatureFlagsProvider( + TEST_TOKEN, + config, + mockTracker, + mockLogger, + credentials, + ); + }); + + it("should include Authorization header with service account credentials", async () => { + const expectedAuthHeader = + "Basic " + Buffer.from("sa-username:sa-secret").toString("base64"); + + const scope = nock("https://localhost", { + reqheaders: { + authorization: expectedAuthHeader, + }, + }) + .get("/flags") + .query(true) + .reply(200, { + code: 200, + flags: { + "test-flag": { + variant_key: "on", + variant_value: true, + }, + }, + }); + + await saProvider.getVariant("test-flag", null, TEST_CONTEXT); + + expect(scope.isDone()).toBe(true); + }); + + it("should include project_id in query parameters", async () => { + const scope = nock("https://localhost") + .get("/flags") + .query((queryObject) => { + return queryObject.project_id === "test-project-123"; + }) + .reply(200, { + code: 200, + flags: { + "test-flag": { + variant_key: "on", + variant_value: true, + }, + }, + }); + + await saProvider.getVariant("test-flag", null, TEST_CONTEXT); + + expect(scope.isDone()).toBe(true); + }); + + it("should include both Authorization header and project_id together", async () => { + const expectedAuthHeader = + "Basic " + Buffer.from("sa-username:sa-secret").toString("base64"); + + const scope = nock("https://localhost", { + reqheaders: { + authorization: expectedAuthHeader, + }, + }) + .get("/flags") + .query((queryObject) => { + return queryObject.project_id === "test-project-123"; + }) + .reply(200, { + code: 200, + flags: { + "test-flag": { + variant_key: "on", + variant_value: true, + }, + }, + }); + + await saProvider.getVariant("test-flag", null, TEST_CONTEXT); + + expect(scope.isDone()).toBe(true); + }); + }); }); diff --git a/packages/mixpanel/test/send_request.js b/packages/mixpanel/test/send_request.js index ec10e79f..c094fc2c 100644 --- a/packages/mixpanel/test/send_request.js +++ b/packages/mixpanel/test/send_request.js @@ -318,4 +318,30 @@ describe("send_request", () => { `/import?ip=0&verbose=0&data=e30%3D&api_key=barbaz`, ); }); + + it("sets Authorization header and project_id query param for service account import", () => { + const { ServiceAccountCredentials } = Mixpanel; + const credentials = new ServiceAccountCredentials( + "sa-user", + "sa-secret", + "test-project-123", + ); + const sa_mixpanel = Mixpanel.init("token", { credentials }); + + sa_mixpanel.send_request({ + endpoint: `/import`, + data: { event: `test event` }, + }); + + expect(https.request).toHaveBeenCalledTimes(1); + const request_options = https.request.mock.calls[0][0]; + + // Verify Authorization header is set + expect(request_options.headers.Authorization).toBe( + `Basic ${Buffer.from("sa-user:sa-secret").toString("base64")}`, + ); + + // Verify project_id is in query string + expect(request_options.path).toContain("project_id=test-project-123"); + }); }); From 89329a4db193fef1c4ea58fcaba3cfe0f5637cb7 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:19:11 +0000 Subject: [PATCH 19/23] Greptile reviews --- packages/mixpanel/readme.md | 2 -- packages/mixpanel/test/import.js | 26 +++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/mixpanel/readme.md b/packages/mixpanel/readme.md index cc213a48..90a0a478 100644 --- a/packages/mixpanel/readme.md +++ b/packages/mixpanel/readme.md @@ -200,8 +200,6 @@ mixpanel.track_batch([ // Service accounts provide enhanced security for server-to-server integrations. // This is the PREFERRED authentication method. API secrets are deprecated. // Learn more: https://developer.mixpanel.com/reference/service-accounts-api -const { ServiceAccountCredentials } = Mixpanel; - const credentials = new ServiceAccountCredentials( "YOUR_SERVICE_ACCOUNT_USERNAME", "YOUR_SERVICE_ACCOUNT_SECRET", diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index c70fe39d..f64825e1 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -444,14 +444,34 @@ describe("import with service account credentials", () => { }); it("does not send auth headers for non-import endpoints", () => { + const http_emitter = new events.EventEmitter(); + const res = new events.EventEmitter(); + vi.spyOn(https, "request").mockImplementation((_, cb) => { + cb(res); + return http_emitter; + }); + http_emitter.write = vi.fn(); + http_emitter.end = vi.fn(); + const mixpanel_sa = Mixpanel.init("token", { credentials }); - vi.spyOn(mixpanel_sa, "send_request"); // Track should not use service account credentials mixpanel_sa.track("test event", { distinct_id: "user123" }); - const call_args = mixpanel_sa.send_request.mock.calls[0][0]; - expect(call_args.endpoint).not.toBe("/import"); + expect(https.request).toHaveBeenCalledTimes(1); + const request_options = https.request.mock.calls[0][0]; + + // Verify endpoint is /track, not /import + expect(request_options.path).toContain("/track"); + expect(request_options.path).not.toContain("/import"); + + // Verify Authorization header is NOT set + expect(request_options.headers.Authorization).toBeUndefined(); + + // Verify project_id is NOT in query string + expect(request_options.path).not.toContain("project_id"); + + https.request.mockRestore(); }); }); From 3188c396722017fb62bb9175934edcbbdcee2d97 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:51:20 +0000 Subject: [PATCH 20/23] Fix log spam for multiple clients --- packages/mixpanel/lib/mixpanel-node.js | 18 +++-- .../mixpanel/test/deprecation-warnings.js | 68 ++++++++++++++++++ packages/mixpanel/test/import.js | 72 ------------------- 3 files changed, 76 insertions(+), 82 deletions(-) create mode 100644 packages/mixpanel/test/deprecation-warnings.js diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index eb34e386..e0aa9aae 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -38,6 +38,9 @@ const DEFAULT_CONFIG = { logger: console, }; +// Module-level flag to warn about deprecated auth once per process +let _deprecation_warned = false; + const create_client = function (token, config) { if (!token) { throw new Error( @@ -527,17 +530,12 @@ const create_client = function (token, config) { } } - // Emit deprecation warnings for old auth methods at client creation - if (config && config.secret) { - metrics.config.logger.warn( - "DEPRECATION WARNING: api_secret is deprecated and will be removed in a future version.\n" + - " Please migrate to ServiceAccountCredentials for enhanced security.\n" + - " See: https://developer.mixpanel.com/reference/service-accounts-api", - ); - } - if (config && config.key) { + // Emit deprecation warning once per process for old auth methods + if (!_deprecation_warned && config && (config.secret || config.key)) { + _deprecation_warned = true; + const method = config.secret ? "api_secret" : "api_key"; metrics.config.logger.warn( - "DEPRECATION WARNING: api_key is deprecated and will be removed in a future version.\n" + + `DEPRECATION WARNING: ${method} is deprecated and will be removed in a future version.\n` + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + " See: https://developer.mixpanel.com/reference/service-accounts-api", ); diff --git a/packages/mixpanel/test/deprecation-warnings.js b/packages/mixpanel/test/deprecation-warnings.js new file mode 100644 index 00000000..59d5d591 --- /dev/null +++ b/packages/mixpanel/test/deprecation-warnings.js @@ -0,0 +1,68 @@ +const Mixpanel = require("../lib/mixpanel-node"); + +describe("deprecation warnings", () => { + let mixpanel; + let mockLogger; + + beforeEach(() => { + mockLogger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + }); + + it("warns once per process when using api_secret (first use)", () => { + mixpanel = Mixpanel.init("token", { + secret: "api-secret", + logger: mockLogger, + }); + + // Warning should be emitted at init time + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("DEPRECATION WARNING"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("api_secret is deprecated"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("ServiceAccountCredentials"), + ); + + // Reset mock to verify it doesn't warn again on subsequent requests + mockLogger.warn.mockClear(); + mixpanel.import("test", Date.now(), { distinct_id: "user" }); + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); + + it("does not warn again when creating another client with deprecated auth (already warned)", () => { + // This simulates serverless where multiple clients are created in same process + // The warning should NOT fire again since we already warned in the first test + const anotherMixpanel = Mixpanel.init("token", { + key: "api-key", + logger: mockLogger, + }); + + // No warning because we already warned once in this process + expect(mockLogger.warn).not.toHaveBeenCalled(); + + anotherMixpanel.track("test", { distinct_id: "user" }); + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); + + it("does not warn when using service account credentials", () => { + const { ServiceAccountCredentials } = Mixpanel; + const creds = new ServiceAccountCredentials("user", "secret", "123"); + + mixpanel = Mixpanel.init("token", { + credentials: creds, + logger: mockLogger, + }); + + mixpanel.import("test", Date.now(), { distinct_id: "user" }); + + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/mixpanel/test/import.js b/packages/mixpanel/test/import.js index f64825e1..300ebc89 100644 --- a/packages/mixpanel/test/import.js +++ b/packages/mixpanel/test/import.js @@ -474,75 +474,3 @@ describe("import with service account credentials", () => { https.request.mockRestore(); }); }); - -describe("deprecation warnings", () => { - let mixpanel; - let mockLogger; - - beforeEach(() => { - mockLogger = { - trace: vi.fn(), - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }; - }); - - it("warns when using api_secret", () => { - mixpanel = Mixpanel.init("token", { - secret: "api-secret", - logger: mockLogger, - }); - - // Warning should be emitted at init time, not during import - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("DEPRECATION WARNING"), - ); - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("api_secret is deprecated"), - ); - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("ServiceAccountCredentials"), - ); - - // Reset mock to verify it doesn't warn again on subsequent requests - mockLogger.warn.mockClear(); - mixpanel.import("test", Date.now(), { distinct_id: "user" }); - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); - - it("warns when using api_key", () => { - mixpanel = Mixpanel.init("token", { - key: "api-key", - logger: mockLogger, - }); - - // Warning should be emitted at init time, not during track - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("DEPRECATION WARNING"), - ); - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("api_key is deprecated"), - ); - - // Reset mock to verify it doesn't warn again on subsequent requests - mockLogger.warn.mockClear(); - mixpanel.track("test", { distinct_id: "user" }); - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); - - it("does not warn when using service account credentials", () => { - const { ServiceAccountCredentials } = Mixpanel; - const creds = new ServiceAccountCredentials("user", "secret", "123"); - - mixpanel = Mixpanel.init("token", { - credentials: creds, - logger: mockLogger, - }); - - mixpanel.import("test", Date.now(), { distinct_id: "user" }); - - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); -}); From 66ebec2152680fab6885c9f839c4ad1316b78e93 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:13:20 +0000 Subject: [PATCH 21/23] Add deprecation warnings for api_secret/api_key using process.emitWarning Uses the idiomatic Node.js deprecation mechanism instead of module-level flags. Benefits: - Node.js automatically dedupes by warning code - Respects --no-deprecation and --no-warnings flags - Standard platform mechanism for deprecation warnings - Warning code: MIXPANEL_LEGACY_AUTH_DEPRECATED Users are warned when they use deprecated authentication methods and are directed to migrate to ServiceAccountCredentials for enhanced security. Co-Authored-By: Claude Sonnet 4.5 --- packages/mixpanel/lib/mixpanel-node.js | 20 +++-- .../mixpanel/test/deprecation-warnings.js | 75 ++++++++----------- 2 files changed, 39 insertions(+), 56 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index e0aa9aae..de7d9876 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -38,9 +38,6 @@ const DEFAULT_CONFIG = { logger: console, }; -// Module-level flag to warn about deprecated auth once per process -let _deprecation_warned = false; - const create_client = function (token, config) { if (!token) { throw new Error( @@ -530,14 +527,15 @@ const create_client = function (token, config) { } } - // Emit deprecation warning once per process for old auth methods - if (!_deprecation_warned && config && (config.secret || config.key)) { - _deprecation_warned = true; - const method = config.secret ? "api_secret" : "api_key"; - metrics.config.logger.warn( - `DEPRECATION WARNING: ${method} is deprecated and will be removed in a future version.\n` + - " Please migrate to ServiceAccountCredentials for enhanced security.\n" + - " See: https://developer.mixpanel.com/reference/service-accounts-api", + // Emit deprecation warning for old auth methods + if (config && (config.secret || config.key)) { + const method = config.secret ? 'api_secret' : 'api_key'; + process.emitWarning( + `${method} is deprecated and will be removed in a future version. ` + + 'Please migrate to ServiceAccountCredentials for enhanced security. ' + + 'See: https://developer.mixpanel.com/reference/service-accounts-api', + 'DeprecationWarning', + 'MIXPANEL_LEGACY_AUTH_DEPRECATED' ); } }; diff --git a/packages/mixpanel/test/deprecation-warnings.js b/packages/mixpanel/test/deprecation-warnings.js index 59d5d591..303994a0 100644 --- a/packages/mixpanel/test/deprecation-warnings.js +++ b/packages/mixpanel/test/deprecation-warnings.js @@ -1,68 +1,53 @@ const Mixpanel = require("../lib/mixpanel-node"); describe("deprecation warnings", () => { - let mixpanel; - let mockLogger; + let warnings; + let originalEmitWarning; beforeEach(() => { - mockLogger = { - trace: vi.fn(), - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), + warnings = []; + originalEmitWarning = process.emitWarning; + process.emitWarning = function (message, type, code) { + warnings.push({ message, type, code }); }; }); - it("warns once per process when using api_secret (first use)", () => { - mixpanel = Mixpanel.init("token", { - secret: "api-secret", - logger: mockLogger, - }); + afterEach(() => { + process.emitWarning = originalEmitWarning; + }); - // Warning should be emitted at init time - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("DEPRECATION WARNING"), - ); - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("api_secret is deprecated"), - ); - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("ServiceAccountCredentials"), - ); + it("warns when using api_secret", () => { + Mixpanel.init("token", { secret: "api-secret" }); - // Reset mock to verify it doesn't warn again on subsequent requests - mockLogger.warn.mockClear(); - mixpanel.import("test", Date.now(), { distinct_id: "user" }); - expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(warnings).toHaveLength(1); + expect(warnings[0].type).toBe("DeprecationWarning"); + expect(warnings[0].code).toBe("MIXPANEL_LEGACY_AUTH_DEPRECATED"); + expect(warnings[0].message).toContain("api_secret is deprecated"); + expect(warnings[0].message).toContain("ServiceAccountCredentials"); }); - it("does not warn again when creating another client with deprecated auth (already warned)", () => { - // This simulates serverless where multiple clients are created in same process - // The warning should NOT fire again since we already warned in the first test - const anotherMixpanel = Mixpanel.init("token", { - key: "api-key", - logger: mockLogger, - }); - - // No warning because we already warned once in this process - expect(mockLogger.warn).not.toHaveBeenCalled(); + it("warns when using api_key", () => { + Mixpanel.init("token", { key: "api-key" }); - anotherMixpanel.track("test", { distinct_id: "user" }); - expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(warnings).toHaveLength(1); + expect(warnings[0].type).toBe("DeprecationWarning"); + expect(warnings[0].code).toBe("MIXPANEL_LEGACY_AUTH_DEPRECATED"); + expect(warnings[0].message).toContain("api_key is deprecated"); + expect(warnings[0].message).toContain("ServiceAccountCredentials"); }); it("does not warn when using service account credentials", () => { const { ServiceAccountCredentials } = Mixpanel; const creds = new ServiceAccountCredentials("user", "secret", "123"); - mixpanel = Mixpanel.init("token", { - credentials: creds, - logger: mockLogger, - }); + Mixpanel.init("token", { credentials: creds }); + + expect(warnings).toHaveLength(0); + }); - mixpanel.import("test", Date.now(), { distinct_id: "user" }); + it("does not warn when using no auth", () => { + Mixpanel.init("token"); - expect(mockLogger.warn).not.toHaveBeenCalled(); + expect(warnings).toHaveLength(0); }); }); From 57771178539afb15c66dfe1e228b3adc9f0f1078 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:45:21 +0000 Subject: [PATCH 22/23] Revert back to warning everytime a client is initialized --- packages/mixpanel/lib/mixpanel-node.js | 12 ++-- .../mixpanel/test/deprecation-warnings.js | 66 +++++++++++-------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index de7d9876..6d3148ca 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -527,15 +527,13 @@ const create_client = function (token, config) { } } - // Emit deprecation warning for old auth methods + // Warn about deprecated auth methods if (config && (config.secret || config.key)) { const method = config.secret ? 'api_secret' : 'api_key'; - process.emitWarning( - `${method} is deprecated and will be removed in a future version. ` + - 'Please migrate to ServiceAccountCredentials for enhanced security. ' + - 'See: https://developer.mixpanel.com/reference/service-accounts-api', - 'DeprecationWarning', - 'MIXPANEL_LEGACY_AUTH_DEPRECATED' + metrics.config.logger.warn( + `DEPRECATION WARNING: ${method} is deprecated and will be removed in a future version.\n` + + ' Please migrate to ServiceAccountCredentials for enhanced security.\n' + + ' See: https://developer.mixpanel.com/reference/service-accounts-api' ); } }; diff --git a/packages/mixpanel/test/deprecation-warnings.js b/packages/mixpanel/test/deprecation-warnings.js index 303994a0..a4482342 100644 --- a/packages/mixpanel/test/deprecation-warnings.js +++ b/packages/mixpanel/test/deprecation-warnings.js @@ -1,53 +1,67 @@ const Mixpanel = require("../lib/mixpanel-node"); describe("deprecation warnings", () => { - let warnings; - let originalEmitWarning; + let mockLogger; beforeEach(() => { - warnings = []; - originalEmitWarning = process.emitWarning; - process.emitWarning = function (message, type, code) { - warnings.push({ message, type, code }); + mockLogger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), }; }); - afterEach(() => { - process.emitWarning = originalEmitWarning; - }); - it("warns when using api_secret", () => { - Mixpanel.init("token", { secret: "api-secret" }); + Mixpanel.init("token", { + secret: "api-secret", + logger: mockLogger, + }); - expect(warnings).toHaveLength(1); - expect(warnings[0].type).toBe("DeprecationWarning"); - expect(warnings[0].code).toBe("MIXPANEL_LEGACY_AUTH_DEPRECATED"); - expect(warnings[0].message).toContain("api_secret is deprecated"); - expect(warnings[0].message).toContain("ServiceAccountCredentials"); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("DEPRECATION WARNING"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("api_secret is deprecated"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("ServiceAccountCredentials"), + ); }); it("warns when using api_key", () => { - Mixpanel.init("token", { key: "api-key" }); + Mixpanel.init("token", { + key: "api-key", + logger: mockLogger, + }); - expect(warnings).toHaveLength(1); - expect(warnings[0].type).toBe("DeprecationWarning"); - expect(warnings[0].code).toBe("MIXPANEL_LEGACY_AUTH_DEPRECATED"); - expect(warnings[0].message).toContain("api_key is deprecated"); - expect(warnings[0].message).toContain("ServiceAccountCredentials"); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("DEPRECATION WARNING"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("api_key is deprecated"), + ); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("ServiceAccountCredentials"), + ); }); it("does not warn when using service account credentials", () => { const { ServiceAccountCredentials } = Mixpanel; const creds = new ServiceAccountCredentials("user", "secret", "123"); - Mixpanel.init("token", { credentials: creds }); + Mixpanel.init("token", { + credentials: creds, + logger: mockLogger, + }); - expect(warnings).toHaveLength(0); + expect(mockLogger.warn).not.toHaveBeenCalled(); }); it("does not warn when using no auth", () => { - Mixpanel.init("token"); + Mixpanel.init("token", { logger: mockLogger }); - expect(warnings).toHaveLength(0); + expect(mockLogger.warn).not.toHaveBeenCalled(); }); }); From 0bb7cd14d663e344029e6440541e8b918967f305 Mon Sep 17 00:00:00 2001 From: John La <10409759+lajohn4747@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:07:00 +0000 Subject: [PATCH 23/23] Fix lint --- packages/mixpanel/lib/mixpanel-node.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mixpanel/lib/mixpanel-node.js b/packages/mixpanel/lib/mixpanel-node.js index 6d3148ca..3b3187e1 100644 --- a/packages/mixpanel/lib/mixpanel-node.js +++ b/packages/mixpanel/lib/mixpanel-node.js @@ -529,11 +529,11 @@ const create_client = function (token, config) { // Warn about deprecated auth methods if (config && (config.secret || config.key)) { - const method = config.secret ? 'api_secret' : 'api_key'; + const method = config.secret ? "api_secret" : "api_key"; metrics.config.logger.warn( `DEPRECATION WARNING: ${method} is deprecated and will be removed in a future version.\n` + - ' Please migrate to ServiceAccountCredentials for enhanced security.\n' + - ' See: https://developer.mixpanel.com/reference/service-accounts-api' + " Please migrate to ServiceAccountCredentials for enhanced security.\n" + + " See: https://developer.mixpanel.com/reference/service-accounts-api", ); } };