diff --git a/README.md b/README.md index 92d9c369396..5fac0025775 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,8 @@ linkStyle default opacity:0.5 app_metadata_controller --> messenger; approval_controller --> base_controller; approval_controller --> messenger; + assets_controller --> base_controller; + assets_controller --> messenger; assets_controllers --> account_tree_controller; assets_controllers --> accounts_controller; assets_controllers --> approval_controller; @@ -352,6 +354,7 @@ linkStyle default opacity:0.5 profile_metrics_controller --> messenger; profile_metrics_controller --> polling_controller; profile_metrics_controller --> profile_sync_controller; + profile_metrics_controller --> transaction_controller; profile_sync_controller --> address_book_controller; profile_sync_controller --> base_controller; profile_sync_controller --> keyring_controller; diff --git a/packages/profile-metrics-controller/CHANGELOG.md b/packages/profile-metrics-controller/CHANGELOG.md index ded171418f0..45fe0b1f899 100644 --- a/packages/profile-metrics-controller/CHANGELOG.md +++ b/packages/profile-metrics-controller/CHANGELOG.md @@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `ProfileMetricsController` contructor now accepts an optional `initialDelayDuration` parameter ([#7624](https://github.com/MetaMask/core/pull/7624)) + - The parameter can be used to override the default time-based delay for the first data collection after opt-in +- Add `skipInitialDelay()` method to `ProfileMetricsController` ([#7624](https://github.com/MetaMask/core/pull/7624)) + - The method can be also called through the `ProfileMetricsController:skipInitialDelay` action via messenger + ### Changed +- **BREAKING:** `ProileMetricsControllerMessenger` now requires the `TransactionController:transactionSubmitted` action to be allowed ([#7624](https://github.com/MetaMask/core/pull/7624)) +- Set time-based delay for first `ProfileMetricsController` data collection after opt-in ([#7624](https://github.com/MetaMask/core/pull/7624)) - Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511)) - Bump `@metamask/controller-utils` from `^11.16.0` to `^11.18.0` ([#7534](https://github.com/MetaMask/core/pull/7534), [#7583](https://github.com/MetaMask/core/pull/7583)) - Bump `@metamask/accounts-controller` from `^35.0.0` to `^35.0.1` ([#7604](https://github.com/MetaMask/core/pull/7604)) diff --git a/packages/profile-metrics-controller/package.json b/packages/profile-metrics-controller/package.json index 49ee4262f0f..98aedc547b1 100644 --- a/packages/profile-metrics-controller/package.json +++ b/packages/profile-metrics-controller/package.json @@ -55,6 +55,7 @@ "@metamask/messenger": "^0.3.0", "@metamask/polling-controller": "^16.0.1", "@metamask/profile-sync-controller": "^27.0.0", + "@metamask/transaction-controller": "^62.9.1", "@metamask/utils": "^11.9.0", "async-mutex": "^0.5.0" }, diff --git a/packages/profile-metrics-controller/src/ProfileMetricsController-method-action-types.ts b/packages/profile-metrics-controller/src/ProfileMetricsController-method-action-types.ts new file mode 100644 index 00000000000..81589990bb8 --- /dev/null +++ b/packages/profile-metrics-controller/src/ProfileMetricsController-method-action-types.ts @@ -0,0 +1,21 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { ProfileMetricsController } from './ProfileMetricsController'; + +/** + * Skip the initial delay period by setting the end timestamp to the current time. + * Metrics will be sent on the next poll. + */ +export type ProfileMetricsControllerSkipInitialDelayAction = { + type: `ProfileMetricsController:skipInitialDelay`; + handler: ProfileMetricsController['skipInitialDelay']; +}; + +/** + * Union of all ProfileMetricsController action types. + */ +export type ProfileMetricsControllerMethodActions = + ProfileMetricsControllerSkipInitialDelayAction; diff --git a/packages/profile-metrics-controller/src/ProfileMetricsController.test.ts b/packages/profile-metrics-controller/src/ProfileMetricsController.test.ts index 44e3e2d9f8a..350cfe5a6fc 100644 --- a/packages/profile-metrics-controller/src/ProfileMetricsController.test.ts +++ b/packages/profile-metrics-controller/src/ProfileMetricsController.test.ts @@ -7,7 +7,10 @@ import type { MessengerEvents, } from '@metamask/messenger'; -import { ProfileMetricsController } from './ProfileMetricsController'; +import { + DEFAULT_INITIAL_DELAY_DURATION, + ProfileMetricsController, +} from './ProfileMetricsController'; import type { ProfileMetricsControllerMessenger } from './ProfileMetricsController'; import type { ProfileMetricsSubmitMetricsRequest, @@ -59,7 +62,7 @@ describe('ProfileMetricsController', () => { describe('constructor subscriptions', () => { describe('when KeyringController:unlock is published', () => { - it('starts polling if the user opted-in', async () => { + it('starts polling immediately', async () => { await withController( { options: { assertUserOptedIn: () => true } }, async ({ controller, rootMessenger }) => { @@ -72,15 +75,13 @@ describe('ProfileMetricsController', () => { ); }); - it('does not start polling if the user has not opted-in', async () => { + it('disables the initial delay if the user has opted in to profile metrics', async () => { await withController( - { options: { assertUserOptedIn: () => false } }, + { options: { assertUserOptedIn: () => true } }, async ({ controller, rootMessenger }) => { - const pollSpy = jest.spyOn(controller, 'startPolling'); - rootMessenger.publish('KeyringController:unlock'); - expect(pollSpy).not.toHaveBeenCalled(); + expect(controller.state.initialDelayEndTimestamp).toBe(Date.now()); }, ); }); @@ -180,6 +181,32 @@ describe('ProfileMetricsController', () => { }); }); + describe('when TransactionController:transactionSubmitted is published', () => { + it('sets `initialDelayEndTimestamp` to current timestamp to skip the initial delay on the next poll', async () => { + await withController( + { + options: { + state: { + initialDelayEndTimestamp: + Date.now() + DEFAULT_INITIAL_DELAY_DURATION, + }, + }, + }, + async ({ controller, rootMessenger }) => { + rootMessenger.publish( + 'TransactionController:transactionSubmitted', + { + // @ts-expect-error Transaction object not needed for this test + foo: 'bar', + }, + ); + + expect(controller.state.initialDelayEndTimestamp).toBe(Date.now()); + }, + ); + }); + }); + describe('when AccountsController:accountAdded is published', () => { describe.each([ { assertUserOptedIn: true }, @@ -306,6 +333,24 @@ describe('ProfileMetricsController', () => { }); }); + describe('skipInitialDelay', () => { + it('sets the initial delay end timestamp to the current time', async () => { + const pastTimestamp = Date.now() - 10000; + await withController( + { + options: { + state: { initialDelayEndTimestamp: pastTimestamp }, + }, + }, + async ({ controller }) => { + controller.skipInitialDelay(); + + expect(controller.state.initialDelayEndTimestamp).toBe(Date.now()); + }, + ); + }); + }); + describe('_executePoll', () => { describe('when the user has not opted in to profile metrics', () => { it('does not process the sync queue', async () => { @@ -329,159 +374,248 @@ describe('ProfileMetricsController', () => { }); }); - describe('when the user has opted in to profile metrics', () => { - it('processes the sync queue on each poll', async () => { + describe('when the initial delay period has not ended', () => { + it('does not process the sync queue', async () => { const accounts: Record = { id1: [{ address: '0xAccount1', scopes: ['eip155:1'] }], }; await withController( { - options: { state: { syncQueue: accounts } }, + options: { + state: { + syncQueue: accounts, + }, + }, }, - async ({ controller, getMetaMetricsId, mockSubmitMetrics }) => { + async ({ controller, mockSubmitMetrics }) => { await controller._executePoll(); - expect(mockSubmitMetrics).toHaveBeenCalledTimes(1); - expect(mockSubmitMetrics).toHaveBeenCalledWith({ - metametricsId: getMetaMetricsId(), - entropySourceId: 'id1', - accounts: [{ address: '0xAccount1', scopes: ['eip155:1'] }], - }); - expect(controller.state.syncQueue).toStrictEqual({}); + expect(mockSubmitMetrics).not.toHaveBeenCalled(); + expect(controller.state.syncQueue).toStrictEqual(accounts); }, ); }); + }); - it('processes the sync queue in batches grouped by entropySourceId', async () => { - const accounts: Record = { - id1: [ - { address: '0xAccount1', scopes: ['eip155:1'] }, - { address: '0xAccount2', scopes: ['eip155:1'] }, - ], - id2: [{ address: '0xAccount3', scopes: ['eip155:1'] }], - null: [{ address: '0xAccount4', scopes: ['eip155:1'] }], - }; + describe('when the user has opted in to profile metrics', () => { + it('sets the correct default initial delay end timestamp if not set yet', async () => { + await withController(async ({ controller }) => { + await controller._executePoll(); + + expect(controller.state.initialDelayEndTimestamp).toBe( + Date.now() + DEFAULT_INITIAL_DELAY_DURATION, + ); + }); + }); + + it('sets a custom initial delay end timestamp if provided via options', async () => { + const customDelay = 60_000; await withController( { - options: { state: { syncQueue: accounts } }, + options: { + initialDelayDuration: customDelay, + }, }, - async ({ controller, getMetaMetricsId, mockSubmitMetrics }) => { + async ({ controller }) => { await controller._executePoll(); - expect(mockSubmitMetrics).toHaveBeenCalledTimes(3); - expect(mockSubmitMetrics).toHaveBeenNthCalledWith(1, { - metametricsId: getMetaMetricsId(), - entropySourceId: 'id1', - accounts: [ - { address: '0xAccount1', scopes: ['eip155:1'] }, - { address: '0xAccount2', scopes: ['eip155:1'] }, - ], - }); - expect(mockSubmitMetrics).toHaveBeenNthCalledWith(2, { - metametricsId: getMetaMetricsId(), - entropySourceId: 'id2', - accounts: [{ address: '0xAccount3', scopes: ['eip155:1'] }], - }); - expect(mockSubmitMetrics).toHaveBeenNthCalledWith(3, { - metametricsId: getMetaMetricsId(), - entropySourceId: null, - accounts: [{ address: '0xAccount4', scopes: ['eip155:1'] }], - }); - expect(controller.state.syncQueue).toStrictEqual({}); + expect(controller.state.initialDelayEndTimestamp).toBe( + Date.now() + customDelay, + ); }, ); }); - it('skips one of the batches if the :submitMetrics call fails, but continues processing the rest', async () => { - const accounts: Record = { - id1: [{ address: '0xAccount1', scopes: ['eip155:1'] }], - id2: [{ address: '0xAccount2', scopes: ['eip155:1'] }], - }; + it('retains the existing initial delay end timestamp if already set', async () => { + const pastTimestamp = Date.now() - 10000; await withController( { - options: { state: { syncQueue: accounts } }, + options: { + state: { initialDelayEndTimestamp: pastTimestamp }, + }, }, - async ({ controller, getMetaMetricsId, mockSubmitMetrics }) => { - const consoleErrorSpy = jest.spyOn(console, 'error'); - mockSubmitMetrics.mockImplementationOnce(() => { - throw new Error('Network error'); - }); - + async ({ controller }) => { await controller._executePoll(); - expect(mockSubmitMetrics).toHaveBeenCalledTimes(2); - expect(mockSubmitMetrics).toHaveBeenNthCalledWith(1, { - metametricsId: getMetaMetricsId(), - entropySourceId: 'id1', - accounts: [{ address: '0xAccount1', scopes: ['eip155:1'] }], - }); - expect(mockSubmitMetrics).toHaveBeenNthCalledWith(2, { - metametricsId: getMetaMetricsId(), - entropySourceId: 'id2', - accounts: [{ address: '0xAccount2', scopes: ['eip155:1'] }], - }); - expect(controller.state.syncQueue).toStrictEqual({ - id1: [{ address: '0xAccount1', scopes: ['eip155:1'] }], - }); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Failed to submit profile metrics for entropy source ID id1:', - expect.any(Error), + expect(controller.state.initialDelayEndTimestamp).toBe( + pastTimestamp, ); }, ); }); + + describe('when the initial delay period has ended', () => { + it('processes the sync queue on each poll', async () => { + const accounts: Record = { + id1: [{ address: '0xAccount1', scopes: ['eip155:1'] }], + }; + await withController( + { + options: { + state: { syncQueue: accounts, initialDelayEndTimestamp: 0 }, + }, + }, + async ({ controller, getMetaMetricsId, mockSubmitMetrics }) => { + await controller._executePoll(); + + expect(mockSubmitMetrics).toHaveBeenCalledTimes(1); + expect(mockSubmitMetrics).toHaveBeenCalledWith({ + metametricsId: getMetaMetricsId(), + entropySourceId: 'id1', + accounts: [{ address: '0xAccount1', scopes: ['eip155:1'] }], + }); + expect(controller.state.syncQueue).toStrictEqual({}); + }, + ); + }); + + it('processes the sync queue in batches grouped by entropySourceId', async () => { + const accounts: Record = { + id1: [ + { address: '0xAccount1', scopes: ['eip155:1'] }, + { address: '0xAccount2', scopes: ['eip155:1'] }, + ], + id2: [{ address: '0xAccount3', scopes: ['eip155:1'] }], + null: [{ address: '0xAccount4', scopes: ['eip155:1'] }], + }; + await withController( + { + options: { + state: { syncQueue: accounts, initialDelayEndTimestamp: 0 }, + }, + }, + async ({ controller, getMetaMetricsId, mockSubmitMetrics }) => { + await controller._executePoll(); + + expect(mockSubmitMetrics).toHaveBeenCalledTimes(3); + expect(mockSubmitMetrics).toHaveBeenNthCalledWith(1, { + metametricsId: getMetaMetricsId(), + entropySourceId: 'id1', + accounts: [ + { address: '0xAccount1', scopes: ['eip155:1'] }, + { address: '0xAccount2', scopes: ['eip155:1'] }, + ], + }); + expect(mockSubmitMetrics).toHaveBeenNthCalledWith(2, { + metametricsId: getMetaMetricsId(), + entropySourceId: 'id2', + accounts: [{ address: '0xAccount3', scopes: ['eip155:1'] }], + }); + expect(mockSubmitMetrics).toHaveBeenNthCalledWith(3, { + metametricsId: getMetaMetricsId(), + entropySourceId: null, + accounts: [{ address: '0xAccount4', scopes: ['eip155:1'] }], + }); + expect(controller.state.syncQueue).toStrictEqual({}); + }, + ); + }); + + it('skips one of the batches if the :submitMetrics call fails, but continues processing the rest', async () => { + const accounts: Record = { + id1: [{ address: '0xAccount1', scopes: ['eip155:1'] }], + id2: [{ address: '0xAccount2', scopes: ['eip155:1'] }], + }; + await withController( + { + options: { + state: { syncQueue: accounts, initialDelayEndTimestamp: 0 }, + }, + }, + async ({ controller, getMetaMetricsId, mockSubmitMetrics }) => { + const consoleErrorSpy = jest.spyOn(console, 'error'); + mockSubmitMetrics.mockImplementationOnce(() => { + throw new Error('Network error'); + }); + + await controller._executePoll(); + + expect(mockSubmitMetrics).toHaveBeenCalledTimes(2); + expect(mockSubmitMetrics).toHaveBeenNthCalledWith(1, { + metametricsId: getMetaMetricsId(), + entropySourceId: 'id1', + accounts: [{ address: '0xAccount1', scopes: ['eip155:1'] }], + }); + expect(mockSubmitMetrics).toHaveBeenNthCalledWith(2, { + metametricsId: getMetaMetricsId(), + entropySourceId: 'id2', + accounts: [{ address: '0xAccount2', scopes: ['eip155:1'] }], + }); + expect(controller.state.syncQueue).toStrictEqual({ + id1: [{ address: '0xAccount1', scopes: ['eip155:1'] }], + }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Failed to submit profile metrics for entropy source ID id1:', + expect.any(Error), + ); + }, + ); + }); + }); }); }); describe('metadata', () => { it('includes expected state in debug snapshots', async () => { - await withController(({ controller }) => { - expect( - deriveStateFromMetadata( - controller.state, - controller.metadata, - 'includeInDebugSnapshot', - ), - ).toMatchInlineSnapshot(` - Object { - "initialEnqueueCompleted": false, - } - `); - }); + await withController( + { options: { state: { initialDelayEndTimestamp: 10 } } }, + ({ controller }) => { + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'includeInDebugSnapshot', + ), + ).toMatchInlineSnapshot(` + Object { + "initialDelayEndTimestamp": 10, + "initialEnqueueCompleted": false, + } + `); + }, + ); }); it('includes expected state in state logs', async () => { - await withController(({ controller }) => { - expect( - deriveStateFromMetadata( - controller.state, - controller.metadata, - 'includeInStateLogs', - ), - ).toMatchInlineSnapshot(` - Object { - "initialEnqueueCompleted": false, - "syncQueue": Object {}, - } - `); - }); + await withController( + { options: { state: { initialDelayEndTimestamp: 10 } } }, + ({ controller }) => { + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'includeInStateLogs', + ), + ).toMatchInlineSnapshot(` + Object { + "initialDelayEndTimestamp": 10, + "initialEnqueueCompleted": false, + "syncQueue": Object {}, + } + `); + }, + ); }); it('persists expected state', async () => { - await withController(({ controller }) => { - expect( - deriveStateFromMetadata( - controller.state, - controller.metadata, - 'persist', - ), - ).toMatchInlineSnapshot(` - Object { - "initialEnqueueCompleted": false, - "syncQueue": Object {}, - } + await withController( + { options: { state: { initialDelayEndTimestamp: 10 } } }, + ({ controller }) => { + expect( + deriveStateFromMetadata( + controller.state, + controller.metadata, + 'persist', + ), + ).toMatchInlineSnapshot(` + Object { + "initialDelayEndTimestamp": 10, + "initialEnqueueCompleted": false, + "syncQueue": Object {}, + } `); - }); + }, + ); }); it('exposes expected state to UI', async () => { @@ -565,6 +699,7 @@ function getMessenger( 'KeyringController:lock', 'AccountsController:accountAdded', 'AccountsController:accountRemoved', + 'TransactionController:transactionSubmitted', ], }); return messenger; diff --git a/packages/profile-metrics-controller/src/ProfileMetricsController.ts b/packages/profile-metrics-controller/src/ProfileMetricsController.ts index 4ba70b8ad36..c434bcbc852 100644 --- a/packages/profile-metrics-controller/src/ProfileMetricsController.ts +++ b/packages/profile-metrics-controller/src/ProfileMetricsController.ts @@ -15,9 +15,11 @@ import type { import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { Messenger } from '@metamask/messenger'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { TransactionControllerTransactionSubmittedEvent } from '@metamask/transaction-controller'; import { Mutex } from 'async-mutex'; import type { ProfileMetricsServiceMethodActions } from '.'; +import type { ProfileMetricsControllerMethodActions } from '.'; import type { AccountWithScopes } from './ProfileMetricsService'; /** @@ -27,6 +29,11 @@ import type { AccountWithScopes } from './ProfileMetricsService'; */ export const controllerName = 'ProfileMetricsController'; +/** + * The default delay duration before data is sent for the first time, in milliseconds. + */ +export const DEFAULT_INITIAL_DELAY_DURATION = 10 * 60 * 1000; // 10 minutes + /** * Describes the shape of the state object for {@link ProfileMetricsController}. */ @@ -43,6 +50,10 @@ export type ProfileMetricsControllerState = { * source ID are grouped under the key "null". */ syncQueue: Record; + /** + * The timestamp when the first data sending can be attempted. + */ + initialDelayEndTimestamp?: number; }; /** @@ -61,6 +72,12 @@ const profileMetricsControllerMetadata = { includeInStateLogs: true, usedInUi: false, }, + initialDelayEndTimestamp: { + persist: true, + includeInDebugSnapshot: true, + includeInStateLogs: true, + usedInUi: false, + }, } satisfies StateMetadata; /** @@ -78,7 +95,7 @@ export function getDefaultProfileMetricsControllerState(): ProfileMetricsControl }; } -const MESSENGER_EXPOSED_METHODS = [] as const; +const MESSENGER_EXPOSED_METHODS = ['skipInitialDelay'] as const; /** * Retrieves the state of the {@link ProfileMetricsController}. @@ -93,7 +110,7 @@ export type ProfileMetricsControllerGetStateAction = ControllerGetStateAction< */ export type ProfileMetricsControllerActions = | ProfileMetricsControllerGetStateAction - | ProfileMetricsServiceMethodActions; + | ProfileMetricsControllerMethodActions; /** * Actions from other messengers that {@link ProfileMetricsControllerMessenger} calls. @@ -125,7 +142,8 @@ type AllowedEvents = | KeyringControllerUnlockEvent | KeyringControllerLockEvent | AccountsControllerAccountAddedEvent - | AccountsControllerAccountRemovedEvent; + | AccountsControllerAccountRemovedEvent + | TransactionControllerTransactionSubmittedEvent; /** * The messenger restricted to actions and events accessed by @@ -148,6 +166,8 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< readonly #getMetaMetricsId: () => string; + readonly #initialDelayDuration: number; + /** * Constructs a new {@link ProfileMetricsController}. * @@ -162,6 +182,8 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< * of the user. * @param args.interval - The interval, in milliseconds, at which the controller will * attempt to send user profile data. Defaults to 10 seconds. + * @param args.initialDelayDuration - The delay duration before data is sent + * for the first time, in milliseconds. Defaults to 10 minutes. */ constructor({ messenger, @@ -169,12 +191,14 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< assertUserOptedIn, getMetaMetricsId, interval = 10 * 1000, + initialDelayDuration = DEFAULT_INITIAL_DELAY_DURATION, }: { messenger: ProfileMetricsControllerMessenger; state?: Partial; interval?: number; assertUserOptedIn: () => boolean; getMetaMetricsId: () => string; + initialDelayDuration?: number; }) { super({ messenger, @@ -188,6 +212,7 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< this.#assertUserOptedIn = assertUserOptedIn; this.#getMetaMetricsId = getMetaMetricsId; + this.#initialDelayDuration = initialDelayDuration; this.messenger.registerMethodActionHandlers( this, @@ -195,15 +220,22 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< ); this.messenger.subscribe('KeyringController:unlock', () => { - this.#queueFirstSyncIfNeeded().catch(console.error); if (this.#assertUserOptedIn()) { - this.startPolling(null); + // If the user has already opted in at the start of the session, + // it must have opted in during onboarding, or during a previous session. + this.skipInitialDelay(); } + this.#queueFirstSyncIfNeeded().catch(console.error); + this.startPolling(null); }); - this.messenger.subscribe('KeyringController:lock', () => { - this.stopAllPolling(); - }); + this.messenger.subscribe('KeyringController:lock', () => + this.stopAllPolling(), + ); + + this.messenger.subscribe('TransactionController:transactionSubmitted', () => + this.skipInitialDelay(), + ); this.messenger.subscribe('AccountsController:accountAdded', (account) => { this.#addAccountToQueue(account).catch(console.error); @@ -216,6 +248,16 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< this.setIntervalLength(interval); } + /** + * Skip the initial delay period by setting the end timestamp to the current time. + * Metrics will be sent on the next poll. + */ + skipInitialDelay(): void { + this.update((state) => { + state.initialDelayEndTimestamp = Date.now(); + }); + } + /** * Execute a single poll to sync user profile data. * @@ -230,6 +272,10 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< if (!this.#assertUserOptedIn()) { return; } + this.#setInitialDelayEndTimestampIfNull(); + if (!this.#isInitialDelayComplete()) { + return; + } for (const [entropySourceId, accounts] of Object.entries( this.state.syncQueue, )) { @@ -283,6 +329,32 @@ export class ProfileMetricsController extends StaticIntervalPollingController()< }); } + /** + * Set the initial delay end timestamp if it is not already set. + */ + #setInitialDelayEndTimestampIfNull(): void { + this.update((state) => { + state.initialDelayEndTimestamp ??= + Date.now() + this.#initialDelayDuration; + }); + } + + /** + * Check if the initial delay end timestamp is in the past. + * + * @returns True if the initial delay period has completed, false otherwise. + */ + #isInitialDelayComplete(): boolean { + // The following check should never be true due to the initialization logic, + // as the `initialDelayEndTimestamp` is always set in the constructor, + // but is included for type safety. Ignoring for code coverage purposes. + // istanbul ignore if + if (this.state.initialDelayEndTimestamp === undefined) { + return false; + } + return Date.now() >= this.state.initialDelayEndTimestamp; + } + /** * Queue the given account to be synced at the next poll. * diff --git a/packages/profile-metrics-controller/src/index.ts b/packages/profile-metrics-controller/src/index.ts index a790d88548f..a8a928733bf 100644 --- a/packages/profile-metrics-controller/src/index.ts +++ b/packages/profile-metrics-controller/src/index.ts @@ -18,3 +18,7 @@ export type { } from './ProfileMetricsService'; export { ProfileMetricsService, serviceName } from './ProfileMetricsService'; export type { ProfileMetricsServiceMethodActions } from './ProfileMetricsService-method-action-types'; +export type { + ProfileMetricsControllerMethodActions, + ProfileMetricsControllerSkipInitialDelayAction, +} from './ProfileMetricsController-method-action-types'; diff --git a/packages/profile-metrics-controller/tsconfig.build.json b/packages/profile-metrics-controller/tsconfig.build.json index 31d19fe3b86..c8d6956fe1e 100644 --- a/packages/profile-metrics-controller/tsconfig.build.json +++ b/packages/profile-metrics-controller/tsconfig.build.json @@ -12,7 +12,8 @@ { "path": "../../packages/keyring-controller/tsconfig.build.json" }, { "path": "../../packages/messenger/tsconfig.build.json" }, { "path": "../../packages/polling-controller/tsconfig.build.json" }, - { "path": "../../packages/profile-sync-controller/tsconfig.build.json" } + { "path": "../../packages/profile-sync-controller/tsconfig.build.json" }, + { "path": "../../packages/transaction-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/profile-metrics-controller/tsconfig.json b/packages/profile-metrics-controller/tsconfig.json index 9b01f9256c0..56a9c79ec6c 100644 --- a/packages/profile-metrics-controller/tsconfig.json +++ b/packages/profile-metrics-controller/tsconfig.json @@ -10,7 +10,8 @@ { "path": "../../packages/keyring-controller" }, { "path": "../../packages/messenger" }, { "path": "../../packages/polling-controller" }, - { "path": "../../packages/profile-sync-controller" } + { "path": "../../packages/profile-sync-controller" }, + { "path": "../../packages/transaction-controller" } ], "include": ["../../types", "./src"], /** diff --git a/yarn.lock b/yarn.lock index 878193cad39..31fbed86fec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4487,6 +4487,7 @@ __metadata: "@metamask/messenger": "npm:^0.3.0" "@metamask/polling-controller": "npm:^16.0.1" "@metamask/profile-sync-controller": "npm:^27.0.0" + "@metamask/transaction-controller": "npm:^62.9.1" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^27.4.1"