Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 35 additions & 10 deletions packages/mixpanel/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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",
});
81 changes: 81 additions & 0 deletions packages/mixpanel/lib/credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* 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, 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 TypeError("Service account username cannot be empty");
}
if (!trimmedSecret) {
throw new TypeError("Service account secret cannot be empty");
}
if (!trimmedProjectId) {
throw new TypeError("Service account project_id cannot be empty");
}

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",
);
}

/**
* Convert credentials to HTTP Basic Auth format.
*
* @returns {string} Base64-encoded username:secret for Authorization header
*/
toHttpBasicAuth() {
return this._cachedAuth;
}

/**
* String representation of credentials (masks secret).
*
* @returns {string}
*/
toString() {
return `ServiceAccountCredentials(username=${this.username}, project_id=${this.project_id}, secret=***)`;
}
}

module.exports = { ServiceAccountCredentials };
13 changes: 10 additions & 3 deletions packages/mixpanel/lib/flags/flags.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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,
);

/**
Expand Down
40 changes: 32 additions & 8 deletions packages/mixpanel/lib/flags/flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
EXPOSURE_EVENT,
REQUEST_HEADERS,
} = require("./utils");
const { ServiceAccountCredentials } = require("../credentials");

/**
* @typedef {import('./types').SelectedVariant} SelectedVariant
Expand All @@ -23,13 +24,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;
}

/**
Expand All @@ -39,10 +49,26 @@ class FeatureFlagsProvider {
*/
async callFlagsEndpoint(additionalParams = null) {
return new Promise((resolve, reject) => {
const commonParams = prepareCommonQueryParams(
this.providerConfig.token,
packageInfo.version,
);
let authHeader;
let commonParams;

if (this.credentials instanceof ServiceAccountCredentials) {
commonParams = prepareCommonQueryParams(
this.providerConfig.token,
packageInfo.version,
this.credentials.project_id,
);
authHeader = "Basic " + this.credentials.toHttpBasicAuth();
Comment thread
lajohn4747 marked this conversation as resolved.
} else {
commonParams = prepareCommonQueryParams(
this.providerConfig.token,
packageInfo.version,
);
authHeader =
"Basic " +
Buffer.from(this.providerConfig.token + ":").toString("base64");
}

const params = new URLSearchParams(commonParams);

if (additionalParams) {
Expand All @@ -60,9 +86,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,
Expand Down
3 changes: 2 additions & 1 deletion packages/mixpanel/lib/flags/local_flags.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +20,7 @@ export default class LocalFeatureFlagsProvider {
callback: (err?: Error) => void,
) => void,
logger: CustomLogger,
credentials?: ServiceAccountCredentials,
);

/**
Expand Down
12 changes: 10 additions & 2 deletions packages/mixpanel/lib/flags/local_flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions packages/mixpanel/lib/flags/remote_flags.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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,
);

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/mixpanel/lib/flags/remote_flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
}

/**
Expand Down
12 changes: 10 additions & 2 deletions packages/mixpanel/lib/flags/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,22 @@ function normalizedHash(key, salt) {
* Prepare common query parameters for feature flags API requests
* @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
*/
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 - add project_id
params.project_id = project_id;
}

return params;
}

/**
Expand Down
17 changes: 15 additions & 2 deletions packages/mixpanel/lib/mixpanel-node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@ 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 function init(
mixpanelToken: string,
config?: Partial<InitConfig>,
): 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;

Expand All @@ -26,6 +38,7 @@ declare namespace mixpanel {
protocol: string;
path: string;
secret: string;
credentials?: ServiceAccountCredentials;
keepAlive: boolean;
geolocate: boolean;
logger: CustomLogger;
Expand Down
Loading
Loading