Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions packages/analytics-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `AnalyticsInvocationOptions` on `trackEvent`, `identify`, and `trackView` to forward context, callback, messageId, and timestamp to `AnalyticsPlatformAdapter` implementations. ([#8701](https://github.com/MetaMask/core/pull/8701))

### Changed

- Bump `@metamask/messenger` from `^1.0.0` to `^1.2.0` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373), [#8632](https://github.com/MetaMask/core/pull/8632))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { AnalyticsController } from './AnalyticsController';
* Events are only tracked if analytics is enabled.
*
* @param event - Analytics event with properties and sensitive properties
* @param options - Optional invocation metadata forwarded to the platform adapter
*/
export type AnalyticsControllerTrackEventAction = {
type: `AnalyticsController:trackEvent`;
Expand All @@ -21,6 +22,7 @@ export type AnalyticsControllerTrackEventAction = {
* Identify a user for analytics.
*
* @param traits - User traits/properties
* @param options - Optional invocation metadata forwarded to the platform adapter
*/
export type AnalyticsControllerIdentifyAction = {
type: `AnalyticsController:identify`;
Expand All @@ -32,6 +34,7 @@ export type AnalyticsControllerIdentifyAction = {
*
* @param name - The identifier/name of the page or screen being viewed (e.g., "home", "settings", "wallet")
* @param properties - Optional properties associated with the view
* @param options - Optional invocation metadata forwarded to the platform adapter
*/
export type AnalyticsControllerTrackViewAction = {
type: `AnalyticsController:trackView`;
Expand Down
246 changes: 212 additions & 34 deletions packages/analytics-controller/src/AnalyticsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
AnalyticsPlatformAdapter,
AnalyticsTrackingEvent,
AnalyticsControllerState,
AnalyticsInvocationOptions,
} from '.';
import { isValidUUIDv4 } from './analyticsControllerStateValidator';

Expand Down Expand Up @@ -276,6 +277,7 @@ describe('AnalyticsController', () => {
sensitive_prop: 'sensitive value',
anonymous: true,
}),
undefined,
);
});

Expand Down Expand Up @@ -584,9 +586,40 @@ describe('AnalyticsController', () => {
const event = createTestEvent('test_event', { prop: 'value' });
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledWith('test_event', {
prop: 'value',
expect(mockAdapter.track).toHaveBeenCalledWith(
'test_event',
{
prop: 'value',
},
undefined,
);
});

it('forwards invocation options to the platform adapter', async () => {
const mockAdapter = createMockAdapter();
const { controller } = await setupController({
state: {
optedIn: true,
analyticsId: '66666666-6666-4666-a666-666666666666',
},
platformAdapter: mockAdapter,
});

const event = createTestEvent('test_event', { prop: 'value' });
const options: AnalyticsInvocationOptions = {
context: { page: { title: 'Unit test' } },
callback: jest.fn(),
messageId: 'be7ac049-0225-4f72-9af0-a79772392b69',
timestamp: '2024-01-15T12:00:00.000Z',
};

controller.trackEvent(event, options);

expect(mockAdapter.track).toHaveBeenCalledWith(
'test_event',
{ prop: 'value' },
options,
);
});

it('tracks event without properties when event has no properties', async () => {
Expand All @@ -602,7 +635,11 @@ describe('AnalyticsController', () => {
const event = createTestEvent('test_event', {}, {}, true);
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledWith('test_event');
expect(mockAdapter.track).toHaveBeenCalledWith(
'test_event',
undefined,
undefined,
);
});

it('tracks single combined event when isAnonymousEventsFeatureEnabled is disabled', async () => {
Expand All @@ -624,11 +661,15 @@ describe('AnalyticsController', () => {
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledTimes(1);
expect(mockAdapter.track).toHaveBeenCalledWith('test_event', {
prop: 'value',
sensitive_prop: 'sensitive value',
anonymous: true,
});
expect(mockAdapter.track).toHaveBeenCalledWith(
'test_event',
{
prop: 'value',
sensitive_prop: 'sensitive value',
anonymous: true,
},
undefined,
);
});

it('tracks single combined event when isAnonymousEventsFeatureEnabled is disabled and only sensitiveProperties are present', async () => {
Expand All @@ -650,10 +691,14 @@ describe('AnalyticsController', () => {
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledTimes(1);
expect(mockAdapter.track).toHaveBeenCalledWith('test_event', {
sensitive_prop: 'sensitive value',
anonymous: true,
});
expect(mockAdapter.track).toHaveBeenCalledWith(
'test_event',
{
sensitive_prop: 'sensitive value',
anonymous: true,
},
undefined,
);
});

it('does not call platform adapter when disabled', async () => {
Expand Down Expand Up @@ -692,14 +737,24 @@ describe('AnalyticsController', () => {
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledTimes(2);
expect(mockAdapter.track).toHaveBeenNthCalledWith(1, 'test_event', {
prop: 'value',
});
expect(mockAdapter.track).toHaveBeenNthCalledWith(2, 'test_event', {
prop: 'value',
sensitive_prop: 'sensitive value',
anonymous: true,
});
expect(mockAdapter.track).toHaveBeenNthCalledWith(
1,
'test_event',
{
prop: 'value',
},
undefined,
);
expect(mockAdapter.track).toHaveBeenNthCalledWith(
2,
'test_event',
{
prop: 'value',
sensitive_prop: 'sensitive value',
anonymous: true,
},
undefined,
);
});

it('tracks regular properties first, then combined event when only sensitive properties are present', async () => {
Expand All @@ -721,11 +776,21 @@ describe('AnalyticsController', () => {
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledTimes(2);
expect(mockAdapter.track).toHaveBeenNthCalledWith(1, 'test_event', {});
expect(mockAdapter.track).toHaveBeenNthCalledWith(2, 'test_event', {
sensitive_prop: 'sensitive value',
anonymous: true,
});
expect(mockAdapter.track).toHaveBeenNthCalledWith(
1,
'test_event',
{},
undefined,
);
expect(mockAdapter.track).toHaveBeenNthCalledWith(
2,
'test_event',
{
sensitive_prop: 'sensitive value',
anonymous: true,
},
undefined,
);
});

it('tracks only regular properties when no sensitive properties are present', async () => {
Expand All @@ -743,9 +808,13 @@ describe('AnalyticsController', () => {
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledTimes(1);
expect(mockAdapter.track).toHaveBeenCalledWith('test_event', {
prop: 'value',
});
expect(mockAdapter.track).toHaveBeenCalledWith(
'test_event',
{
prop: 'value',
},
undefined,
);
});

it('tracks only regular properties when empty sensitive properties are present', async () => {
Expand All @@ -763,9 +832,57 @@ describe('AnalyticsController', () => {
controller.trackEvent(event);

expect(mockAdapter.track).toHaveBeenCalledTimes(1);
expect(mockAdapter.track).toHaveBeenCalledWith('test_event', {
prop: 'value',
expect(mockAdapter.track).toHaveBeenCalledWith(
'test_event',
{
prop: 'value',
},
undefined,
);
});

it('forwards invocation options with a derived messageId on the anonymous track when splitting sensitive events', async () => {
const mockAdapter = createMockAdapter();
const { controller } = await setupController({
state: {
optedIn: true,
analyticsId: '55555555-5555-4555-9555-555555555555',
},
platformAdapter: mockAdapter,
isAnonymousEventsFeatureEnabled: true,
});

const event = createTestEvent(
'test_event',
{ prop: 'value' },
{ sensitive_prop: 'sensitive value' },
);
const callback = jest.fn();
const options: AnalyticsInvocationOptions = {
messageId: 'c3c3c3c3-c3c3-43c3-8c3c-c3c3c3c3c3c3',
callback,
};
controller.trackEvent(event, options);

expect(mockAdapter.track).toHaveBeenNthCalledWith(
1,
'test_event',
{ prop: 'value' },
options,
);
expect(mockAdapter.track).toHaveBeenNthCalledWith(
2,
'test_event',
{
prop: 'value',
sensitive_prop: 'sensitive value',
anonymous: true,
},
{
...options,
messageId: `${options.messageId}-0x000`,
},
);
});
});
});
Expand All @@ -790,7 +907,11 @@ describe('AnalyticsController', () => {
controller.identify(traits);

expect(controller.state.analyticsId).toBe(analyticsId);
expect(mockAdapter.identify).toHaveBeenCalledWith(analyticsId, traits);
expect(mockAdapter.identify).toHaveBeenCalledWith(
analyticsId,
traits,
undefined,
);
});

it('identifies user without traits', async () => {
Expand All @@ -806,7 +927,37 @@ describe('AnalyticsController', () => {

controller.identify();

expect(mockAdapter.identify).toHaveBeenCalledWith(analyticsId, undefined);
expect(mockAdapter.identify).toHaveBeenCalledWith(
analyticsId,
undefined,
undefined,
);
});

it('forwards invocation options to the platform adapter', async () => {
const mockAdapter = createMockAdapter();
const analyticsId = 'cccccccc-cccc-4ccc-9ccc-cccccccccccc';
const { controller } = await setupController({
state: {
analyticsId,
optedIn: true,
},
platformAdapter: mockAdapter,
});

const traits = { PLAN: 'pro' };
const options: AnalyticsInvocationOptions = {
messageId: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa',
context: { locale: 'en' },
};

controller.identify(traits, options);

expect(mockAdapter.identify).toHaveBeenCalledWith(
analyticsId,
traits,
options,
);
});

it('does not identify when disabled', async () => {
Expand Down Expand Up @@ -843,9 +994,36 @@ describe('AnalyticsController', () => {

controller.trackView('home', { referrer: 'test' });

expect(mockAdapter.view).toHaveBeenCalledWith('home', {
referrer: 'test',
expect(mockAdapter.view).toHaveBeenCalledWith(
'home',
{
referrer: 'test',
},
undefined,
);
});

it('forwards invocation options to the platform adapter', async () => {
const mockAdapter = createMockAdapter();
const { controller } = await setupController({
state: {
optedIn: true,
analyticsId: 'ffffffff-ffff-4fff-8fff-ffffffffffff',
},
platformAdapter: mockAdapter,
});

const options: AnalyticsInvocationOptions = {
timestamp: 1_700_000_000_000,
};

controller.trackView('settings', { section: 'security' }, options);

expect(mockAdapter.view).toHaveBeenCalledWith(
'settings',
{ section: 'security' },
options,
);
});

it('does not call platform adapter when disabled', async () => {
Expand Down
Loading
Loading