Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ claude/
.github/copilot-*
.github/instructions/
.github/prompts/
.devcontainer/
90 changes: 45 additions & 45 deletions __tests__/index.test.js

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions __tests__/jest_setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jest.mock("mixpanel-react-native/javascript/mixpanel-storage", () => {
};
});
jest.mock("uuid", () => ({
v4: jest.fn(),
v4: jest.fn(() => "default-uuid-1234"),
}));

jest.mock("@react-native-async-storage/async-storage", () => ({
Expand All @@ -43,19 +43,19 @@ jest.doMock("react-native", () => {
NativeModules: {
...ReactNative.NativeModules,
MixpanelReactNative: {
initialize: jest.fn(),
initialize: jest.fn().mockResolvedValue(undefined),
setServerURL: jest.fn(),
setLoggingEnabled: jest.fn(),
setFlushOnBackground: jest.fn(),
setUseIpAddressForGeolocation: jest.fn(),
setFlushBatchSize: jest.fn(),
hasOptedOutTracking: jest.fn(),
optInTracking: jest.fn(),
optOutTracking: jest.fn(),
identify: jest.fn(),
alias: jest.fn(),
track: jest.fn(),
trackWithGroups: jest.fn(),
hasOptedOutTracking: jest.fn().mockResolvedValue(false),
optInTracking: jest.fn().mockResolvedValue(undefined),
optOutTracking: jest.fn().mockResolvedValue(undefined),
identify: jest.fn().mockResolvedValue(undefined),
alias: jest.fn().mockResolvedValue(undefined),
track: jest.fn().mockResolvedValue(undefined),
trackWithGroups: jest.fn().mockResolvedValue(undefined),
setGroup: jest.fn(),
getGroup: jest.fn(),
addGroup: jest.fn(),
Expand All @@ -68,8 +68,8 @@ jest.doMock("react-native", () => {
clearSuperProperties: jest.fn(),
timeEvent: jest.fn(),
eventElapsedTime: jest.fn(),
reset: jest.fn(),
getDistinctId: jest.fn(),
reset: jest.fn().mockResolvedValue(undefined),
getDistinctId: jest.fn().mockResolvedValue("test-distinct-id"),
set: jest.fn(),
setOnce: jest.fn(),
increment: jest.fn(),
Expand Down
2 changes: 0 additions & 2 deletions __tests__/main.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { MixpanelType } from "mixpanel-react-native/javascript/mixpanel-constants";
import { exp } from "react-native/Libraries/Animated/src/Easing";
import { get } from "react-native/Libraries/Utilities/PixelRatio";

jest.mock("mixpanel-react-native/javascript/mixpanel-core", () => ({
MixpanelCore: jest.fn().mockImplementation(() => ({
Expand Down
310 changes: 116 additions & 194 deletions __tests__/persistent.test.js
Original file line number Diff line number Diff line change
@@ -1,219 +1,141 @@
// Test UUID generation with polyfilled crypto.getRandomValues
describe("MixpanelPersistent - UUID Generation", () => {
const token = "test-token";
let mockAsyncStorage;

beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();

// Mock AsyncStorage
mockAsyncStorage = {
getItem: jest.fn().mockResolvedValue(null),
setItem: jest.fn().mockResolvedValue(undefined),
removeItem: jest.fn().mockResolvedValue(undefined),
};

// Re-mock storage adapter
jest.doMock("mixpanel-react-native/javascript/mixpanel-storage", () => ({
AsyncStorageAdapter: jest.fn().mockImplementation(() => mockAsyncStorage),
}));
});

afterEach(() => {
jest.restoreAllMocks();
});

it("should generate device ID using uuid.v4() with polyfill", async () => {
let mixpanelPersistent;
// This test verifies that uuid.v4() is called when generating device IDs
const uuid = require("uuid");
const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");

await jest.isolateModules(async () => {
// Mock uuid to return a specific value
jest.doMock("uuid", () => ({
v4: jest.fn(() => "polyfilled-uuid-1234"),
}));

// Now require the modules
const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");
const uuid = require("uuid");

// Create instance
MixpanelPersistent.instance = null;
mixpanelPersistent = MixpanelPersistent.getInstance(null, token);
// Create instance (will use mocked uuid from jest_setup.js)
MixpanelPersistent.instance = null;
const mixpanelPersistent = MixpanelPersistent.getInstance(mockAsyncStorage, token);

// Load device ID (which triggers UUID generation)
await mixpanelPersistent.loadDeviceId(token);
// Load device ID (which triggers UUID generation)
await mixpanelPersistent.loadDeviceId(token);

// Verify uuid.v4 was called
expect(uuid.v4).toHaveBeenCalled();
// Verify uuid.v4 was called
expect(uuid.v4).toHaveBeenCalled();

// Verify the device ID was set
expect(mixpanelPersistent.getDeviceId(token)).toBe("polyfilled-uuid-1234");
});
// Verify the device ID was set to the mocked value
expect(mixpanelPersistent.getDeviceId(token)).toBe("default-uuid-1234");

// Verify it was persisted to storage
expect(mockAsyncStorage.setItem).toHaveBeenCalledWith(
"MIXPANEL_" + token + "_DEVICE_ID",
"default-uuid-1234"
);
});

it("should handle legacy device IDs without expo-crypto format", async () => {
let mixpanelPersistent;

await jest.isolateModules(async () => {
// Create a mock storage with a legacy UUID
const legacyUuid = "550e8400-e29b-41d4-a716-446655440000"; // Standard UUID v4 format
const mockStorage = {};

jest.doMock("mixpanel-react-native/javascript/mixpanel-storage", () => {
return {
AsyncStorageAdapter: jest.fn().mockImplementation(() => ({
getItem: jest.fn((key) => {
return Promise.resolve(mockStorage[key] || null);
}),
setItem: jest.fn((key, value) => {
mockStorage[key] = value;
return Promise.resolve();
}),
removeItem: jest.fn((key) => {
delete mockStorage[key];
return Promise.resolve();
}),
})),
};
});

// Pre-populate storage with legacy device ID
const deviceIdKey = `MIXPANEL_${token}_device_id`;
mockStorage[deviceIdKey] = legacyUuid;

const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");

// Create instance
MixpanelPersistent.instance = null;
mixpanelPersistent = MixpanelPersistent.getInstance(null, token);

// Load device ID from storage
await mixpanelPersistent.loadDeviceId(token);

// Verify the legacy device ID was loaded correctly
expect(mixpanelPersistent.getDeviceId(token)).toBe(legacyUuid);

// Verify it doesn't try to generate a new one
const { randomUUID } = require("expo-crypto");
const uuid = require("uuid");
expect(randomUUID).not.toHaveBeenCalled();
expect(uuid.v4).not.toHaveBeenCalled();
});
it("should handle multiple instances with different tokens", async () => {
const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");
const token1 = "token1";
const token2 = "token2";

// Reset singleton
MixpanelPersistent.instance = null;

// Get instance (singleton)
const instance = MixpanelPersistent.getInstance(mockAsyncStorage, token1);

// Load device IDs for both tokens
await instance.loadDeviceId(token1);
await instance.loadDeviceId(token2);

// Both should have device IDs
expect(instance.getDeviceId(token1)).toBe("default-uuid-1234");
expect(instance.getDeviceId(token2)).toBe("default-uuid-1234");

// Verify both were persisted with different keys
expect(mockAsyncStorage.setItem).toHaveBeenCalledWith(
"MIXPANEL_" + token1 + "_DEVICE_ID",
"default-uuid-1234"
);
expect(mockAsyncStorage.setItem).toHaveBeenCalledWith(
"MIXPANEL_" + token2 + "_DEVICE_ID",
"default-uuid-1234"
);
});

it("should migrate from no device ID to expo-crypto format when available", async () => {
let mixpanelPersistent;

await jest.isolateModules(async () => {
const mockStorage = {};

jest.doMock("mixpanel-react-native/javascript/mixpanel-storage", () => {
return {
AsyncStorageAdapter: jest.fn().mockImplementation(() => ({
getItem: jest.fn((key) => Promise.resolve(mockStorage[key] || null)),
setItem: jest.fn((key, value) => {
mockStorage[key] = value;
return Promise.resolve();
}),
removeItem: jest.fn((key) => {
delete mockStorage[key];
return Promise.resolve();
}),
})),
};
});

// Mock expo-crypto to return a specific value
jest.doMock("expo-crypto", () => ({
randomUUID: jest.fn(() => "expo-generated-uuid"),
}));

const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");
const { randomUUID } = require("expo-crypto");

// Create instance with no existing device ID
MixpanelPersistent.instance = null;
mixpanelPersistent = MixpanelPersistent.getInstance(null, token);

// Load device ID (should generate new one)
await mixpanelPersistent.loadDeviceId(token);

// Verify expo-crypto was used for new device ID
expect(randomUUID).toHaveBeenCalled();
expect(mixpanelPersistent.getDeviceId(token)).toBe("expo-generated-uuid");

// Verify it was persisted to storage
const deviceIdKey = `MIXPANEL_${token}_device_id`;
expect(mockStorage[deviceIdKey]).toBe("expo-generated-uuid");
});
it("should persist and load identity correctly", async () => {
const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");

// Reset singleton
MixpanelPersistent.instance = null;
const instance = MixpanelPersistent.getInstance(mockAsyncStorage, token);

// Load identity first to initialize the structure
await instance.loadIdentity(token);

// Update identity
instance.updateDistinctId(token, "test-distinct-id");
instance.updateUserId(token, "test-user-id");

// Persist identity
await instance.persistIdentity(token);

// Verify identity is correct
expect(instance.getDistinctId(token)).toBe("test-distinct-id");
expect(instance.getUserId(token)).toBe("test-user-id");

// Verify persistence
expect(mockAsyncStorage.setItem).toHaveBeenCalledWith(
"MIXPANEL_" + token + "_DISTINCT_ID",
"test-distinct-id"
);
expect(mockAsyncStorage.setItem).toHaveBeenCalledWith(
"MIXPANEL_" + token + "_USER_ID",
"test-user-id"
);
});

it("should handle storage failures gracefully during identity persistence", async () => {
let mixpanelPersistent;

await jest.isolateModules(async () => {
let shouldFail = false;

jest.doMock("mixpanel-react-native/javascript/mixpanel-storage", () => {
return {
AsyncStorageAdapter: jest.fn().mockImplementation(() => ({
getItem: jest.fn(() => Promise.resolve(null)),
setItem: jest.fn((key, value) => {
if (shouldFail) {
return Promise.reject(new Error("Storage write failed"));
}
return Promise.resolve();
}),
removeItem: jest.fn(() => Promise.resolve()),
})),
};
});

// Mock expo-crypto
jest.doMock("expo-crypto", () => ({
randomUUID: jest.fn(() => "test-uuid-12345"),
}));

const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");

// Create instance
MixpanelPersistent.instance = null;
mixpanelPersistent = MixpanelPersistent.getInstance(null, token);

// First load should work (storage not failing yet)
await mixpanelPersistent.loadDeviceId(token);
expect(mixpanelPersistent.getDeviceId(token)).toBe("test-uuid-12345");

// Now make storage fail
shouldFail = true;

// Update identity and try to persist - should not throw
mixpanelPersistent.updateDistinctId(token, "new-distinct-id");
mixpanelPersistent.updateUserId(token, "new-user-id");

// This should not throw even if storage fails
await expect(mixpanelPersistent.persistIdentity(token)).resolves.not.toThrow();

// Identity should still be in memory even if storage failed
expect(mixpanelPersistent.getDistinctId(token)).toBe("new-distinct-id");
expect(mixpanelPersistent.getUserId(token)).toBe("new-user-id");
expect(mixpanelPersistent.getDeviceId(token)).toBe("test-uuid-12345");
});
it("should verify react-native-get-random-values polyfill is imported", () => {
// The polyfill import is at the top of mixpanel-persistent.js
// This ensures crypto.getRandomValues is available for uuid.v4()
const polyfillModule = jest.requireActual("react-native-get-random-values");
// If this doesn't throw, the module exists and can be imported
expect(polyfillModule).toBeDefined();
});

it("should continue working if storage read fails", async () => {
let mixpanelPersistent;

await jest.isolateModules(async () => {
jest.doMock("mixpanel-react-native/javascript/mixpanel-storage", () => {
return {
AsyncStorageAdapter: jest.fn().mockImplementation(() => ({
getItem: jest.fn(() => Promise.reject(new Error("Storage read failed"))),
setItem: jest.fn(() => Promise.resolve()),
removeItem: jest.fn(() => Promise.resolve()),
})),
};
});

// Mock expo-crypto
jest.doMock("expo-crypto", () => ({
randomUUID: jest.fn(() => "fallback-uuid"),
}));

const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");

// Create instance
MixpanelPersistent.instance = null;
mixpanelPersistent = MixpanelPersistent.getInstance(null, token);

// Loading should not throw even if storage fails
await expect(mixpanelPersistent.loadDeviceId(token)).resolves.not.toThrow();

// Should generate a new device ID since storage failed
expect(mixpanelPersistent.getDeviceId(token)).toBe("fallback-uuid");
});
it("should generate distinct ID based on device ID", async () => {
const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");

// Create instance
MixpanelPersistent.instance = null;
const instance = MixpanelPersistent.getInstance(mockAsyncStorage, token);

// Load identity (loads device ID, distinct ID, and user ID)
await instance.loadIdentity(token);

const deviceId = instance.getDeviceId(token);
const distinctId = instance.getDistinctId(token);

// When no user ID is set, distinct ID should be "$device:" + device ID
expect(distinctId).toBe("$device:" + deviceId);
expect(distinctId).toBe("$device:default-uuid-1234");
});
});
Loading
Loading