Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
247 changes: 51 additions & 196 deletions __tests__/persistent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,216 +4,71 @@ describe("MixpanelPersistent - UUID Generation", () => {

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

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 (will use mocked uuid from jest_setup.js)
MixpanelPersistent.instance = null;
const mixpanelPersistent = MixpanelPersistent.getInstance(null, token);

// Create instance
MixpanelPersistent.instance = null;
mixpanelPersistent = MixpanelPersistent.getInstance(null, 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");
});

it("should handle legacy device IDs without expo-crypto format", async () => {
let mixpanelPersistent;
it("should handle multiple instances with different tokens", async () => {
const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");
const token1 = "token1";
const token2 = "token2";

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 migrate from no device ID to expo-crypto format when available", async () => {
let mixpanelPersistent;
// Reset singleton
MixpanelPersistent.instance = null;

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 handle storage failures gracefully during identity persistence", async () => {
let mixpanelPersistent;
// Get instance (singleton)
const instance = MixpanelPersistent.getInstance(null, token1);

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

it("should continue working if storage read fails", async () => {
let mixpanelPersistent;
it("should persist and load identity correctly", async () => {
const { MixpanelPersistent } = require("mixpanel-react-native/javascript/mixpanel-persistent");

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");
});
// Reset singleton
MixpanelPersistent.instance = null;
const instance = MixpanelPersistent.getInstance(null, 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");
});
});
3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: ['@react-native/babel-preset'],
};
Loading
Loading