diff --git a/package-lock.json b/package-lock.json index 3789e67408..b6345d1999 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,25 +108,35 @@ "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", "dev": true }, - "node_modules/@amplitude/identify": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@amplitude/identify/-/identify-1.10.2.tgz", - "integrity": "sha512-ywxeabS8ukMdJWNwx3rG/EBngXFg/4NsPhlyAxbBUcI7HzBXEJUKepiZfkz8K6Y7f0mpc23Qz1aBf48ZJDZmkQ==", + "node_modules/@amplitude/analytics-core": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-1.2.3.tgz", + "integrity": "sha512-3WADE8IcxU7ZERMkBb0+JP7t6EekyFPM0djtNKXQaxgGgH3oqQzMmBCg19UnYYiBSHrZkpiMBLHNAvXL6HM7zg==", "dependencies": { - "@amplitude/types": "^1.10.2", - "@amplitude/utils": "^1.10.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" + "@amplitude/analytics-types": "^1.3.3", + "tslib": "^2.4.1" } }, - "node_modules/@amplitude/node": { + "node_modules/@amplitude/analytics-node": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-node/-/analytics-node-1.3.3.tgz", + "integrity": "sha512-G+5EoGcVOOclcNRR57AQcGqg46xNXjG6tsSdF71+Npzc6zTfvDzKDY97ZULBju9TT/FXvhP8LnppAI8umT6qkQ==", + "dependencies": { + "@amplitude/analytics-core": "^1.2.3", + "@amplitude/analytics-types": "^1.3.3", + "tslib": "^2.4.1" + } + }, + "node_modules/@amplitude/analytics-types": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-1.3.3.tgz", + "integrity": "sha512-V4/h+izhG7NyVfIva1uhe6bToI/l5n+UnEomL3KEO9DkFoKiOG7KmXo/fmzfU6UmD1bUEWmy//hUFF16BfrEww==" + }, + "node_modules/@amplitude/identify": { "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@amplitude/node/-/node-1.10.2.tgz", - "integrity": "sha512-E3xp8DOpkF5ThjrRlAmSocnrEYsTPpd3Zg4WdBLms0ackQSgQpw6z84+YMcoPerZHJJ/LEqdo4Cg4Z5Za3D+3Q==", + "resolved": "https://registry.npmjs.org/@amplitude/identify/-/identify-1.10.2.tgz", + "integrity": "sha512-ywxeabS8ukMdJWNwx3rG/EBngXFg/4NsPhlyAxbBUcI7HzBXEJUKepiZfkz8K6Y7f0mpc23Qz1aBf48ZJDZmkQ==", "dependencies": { - "@amplitude/identify": "^1.10.2", "@amplitude/types": "^1.10.2", "@amplitude/utils": "^1.10.2", "tslib": "^2.0.0" @@ -30608,7 +30618,8 @@ "version": "2.6.0", "license": "Apache-2.0", "dependencies": { - "@amplitude/node": "1.10.2", + "@amplitude/analytics-node": "^1.3.3", + "@amplitude/analytics-types": "^2.1.2", "@coveo/platform-client": "44.1.0", "@oclif/core": "1.24.0", "abortcontroller-polyfill": "1.7.5", @@ -30645,6 +30656,11 @@ "typescript": "4.9.5" } }, + "packages/cli/commons/node_modules/@amplitude/analytics-types": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.1.2.tgz", + "integrity": "sha512-ASKwH9g+5gglTHr7h7miK8J/ofIzuEtGRDCjnZAtRbE6+laoOfCLYPPJXMYz0k1x+rIhLO/6I6WWjT7zchmpyA==" + }, "packages/cli/commons/node_modules/is-ci": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", @@ -30661,8 +30677,8 @@ "version": "2.6.2", "license": "Apache-2.0", "dependencies": { + "@amplitude/analytics-node": "^1.3.3", "@amplitude/identify": "^1.9.0", - "@amplitude/node": "^1.9.0", "@coveo/cli-commons": "2.6.0", "@coveo/cli-plugin-source": "2.0.12", "@coveo/platform-client": "44.1.0", @@ -30697,6 +30713,7 @@ "coveo": "bin/run" }, "devDependencies": { + "@amplitude/analytics-types": "^2.1.2", "@amplitude/types": "1.10.2", "@babel/core": "7.21.5", "@coveo/angular": "1.36.0", @@ -30746,6 +30763,12 @@ } } }, + "packages/cli/core/node_modules/@amplitude/analytics-types": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.1.2.tgz", + "integrity": "sha512-ASKwH9g+5gglTHr7h7miK8J/ofIzuEtGRDCjnZAtRbE6+laoOfCLYPPJXMYz0k1x+rIhLO/6I6WWjT7zchmpyA==", + "dev": true + }, "packages/cli/source": { "name": "@coveo/cli-plugin-source", "version": "2.0.12", diff --git a/packages/cli/commons/package.json b/packages/cli/commons/package.json index e194e22fcd..52f8b1de3c 100644 --- a/packages/cli/commons/package.json +++ b/packages/cli/commons/package.json @@ -24,7 +24,8 @@ "typescript": "4.9.5" }, "dependencies": { - "@amplitude/node": "1.10.2", + "@amplitude/analytics-node": "^1.3.3", + "@amplitude/analytics-types": "^2.1.2", "@coveo/platform-client": "44.1.0", "@oclif/core": "1.24.0", "abortcontroller-polyfill": "1.7.5", diff --git a/packages/cli/commons/src/analytics/amplitudeClient.ts b/packages/cli/commons/src/analytics/amplitudeClient.ts index 87fd6ade92..67a3d4cbcc 100644 --- a/packages/cli/commons/src/analytics/amplitudeClient.ts +++ b/packages/cli/commons/src/analytics/amplitudeClient.ts @@ -1,14 +1,7 @@ -import {init, NodeClient} from '@amplitude/node'; +import {init} from '@amplitude/analytics-node'; +export {flush, track} from '@amplitude/analytics-node'; const analyticsAPIKey = 'af28cba7acfd392c324bebd399e2d9ea'; -export interface AmplitudeClient extends NodeClient { - identified: boolean; -} - // TODO: CDX-667: support proxy -export const amplitudeClient = init(analyticsAPIKey); - -export const flush = async () => { - await amplitudeClient.flush(); -}; +init(analyticsAPIKey); diff --git a/packages/cli/commons/src/analytics/identifier.spec.ts b/packages/cli/commons/src/analytics/identifier.spec.ts index cfec88316b..790d289639 100644 --- a/packages/cli/commons/src/analytics/identifier.spec.ts +++ b/packages/cli/commons/src/analytics/identifier.spec.ts @@ -1,4 +1,4 @@ -jest.mock('@amplitude/node'); +jest.mock('@amplitude/analytics-node'); jest.mock('@amplitude/identify'); jest.mock('@coveo/platform-client'); jest.mock('../platform/authenticatedClient'); @@ -6,39 +6,36 @@ jest.mock('../config/config'); jest.mock('../config/globalConfig'); import os from 'os'; -import {Identify} from '@amplitude/identify'; +import { + Identify, + identify as amplitudeIdentify, +} from '@amplitude/analytics-node'; import {Config, Configuration} from '../config/config'; import {AuthenticatedClient} from '../platform/authenticatedClient'; import {Identifier} from './identifier'; import PlatformClient from '@coveo/platform-client'; import {configurationMock, defaultConfiguration} from '../config/stub'; import type {Interfaces} from '@oclif/core'; -import type {NodeClient} from '@amplitude/node'; import globalConfig from '../config/globalConfig'; describe('identifier', () => { const mockedGlobalConfig = jest.mocked(globalConfig); const mockedConfig = jest.mocked(Config); - const mockedIdentify = jest.mocked(Identify); + const mockedIdentifyClass = jest.mocked(Identify); + const mockedAmplitudeIdentifyFn = jest.mocked(amplitudeIdentify); const mockedAuthenticatedClient = jest.mocked(AuthenticatedClient); const mockedPlatformClient = jest.mocked(PlatformClient); const mockUserGet = jest.fn(); const mockSetIdentity = jest.fn(); - const mockedLogEvent = jest.fn(); const mockedOsVersion = jest.spyOn(os, 'release'); let identity: Awaited>; - const getDummyAmplitudeClient = () => - ({ - logEvent: mockedLogEvent, - } as unknown as NodeClient); - const doMockOS = () => { mockedOsVersion.mockReturnValue('21.3.4'); }; const doMockIdentify = () => { - mockedIdentify.prototype.set.mockImplementation(mockSetIdentity); + mockedIdentifyClass.prototype.set.mockImplementation(mockSetIdentity); }; const doMockPlatformClient = (email = '') => { mockedPlatformClient.mockImplementation( @@ -191,17 +188,19 @@ describe('identifier', () => { describe('when logging for every user type', () => { beforeEach(async () => { identity = await new Identifier().getIdentity(); - identity.identify(getDummyAmplitudeClient()); + identity.identify(); }); it('should add the CLI version to the event', async () => { - expect(mockedLogEvent).toHaveBeenCalledWith( + expect(mockedAmplitudeIdentifyFn).toHaveBeenCalledWith( + expect.any(Identify), expect.objectContaining({app_version: '1.2.3'}) ); }); it('should add the OS information to the event', async () => { - expect(mockedLogEvent).toHaveBeenCalledWith( + expect(mockedAmplitudeIdentifyFn).toHaveBeenCalledWith( + expect.any(Identify), expect.objectContaining({ app_version: '1.2.3', os_name: 'darwin', diff --git a/packages/cli/commons/src/analytics/identifier.ts b/packages/cli/commons/src/analytics/identifier.ts index a650db8a03..78c544f645 100644 --- a/packages/cli/commons/src/analytics/identifier.ts +++ b/packages/cli/commons/src/analytics/identifier.ts @@ -1,13 +1,16 @@ import os from 'os'; -import {Identify} from '@amplitude/identify'; +import { + Identify, + identify as amplitudeIdentify, +} from '@amplitude/analytics-node'; import {machineId} from 'node-machine-id'; import {createHash} from 'crypto'; import {AuthenticatedClient} from '../platform/authenticatedClient'; import PlatformClient from '@coveo/platform-client'; import {camelToSnakeCase} from '../utils/string'; -import type {NodeClient} from '@amplitude/node'; import globalConfig from '../config/globalConfig'; import {Configuration} from '../config/config'; +import type {EventOptions} from '@amplitude/analytics-types'; export class Identifier { private authenticatedClient: AuthenticatedClient; @@ -35,13 +38,14 @@ export class Identifier { identifier.set(camelToSnakeCase(key), value); }); - const identify = (amplitudeClient: NodeClient) => { - const identifyEvent = { - ...identifier.identifyUser(userId, deviceId), + const identify = () => { + const identifyEvent: EventOptions = { + user_id: userId, + device_id: deviceId, ...this.getAmplitudeBaseEventProperties(), ...this.getOrganizationIdentifier(), }; - amplitudeClient.logEvent(identifyEvent); + amplitudeIdentify(identifier, identifyEvent); }; return {userId, deviceId, identify}; diff --git a/packages/cli/core/package.json b/packages/cli/core/package.json index 8fa4c37087..382717d3ee 100644 --- a/packages/cli/core/package.json +++ b/packages/cli/core/package.json @@ -7,8 +7,8 @@ "node": "^16.13.0 || ^18.12.0" }, "dependencies": { + "@amplitude/analytics-node": "^1.3.3", "@amplitude/identify": "^1.9.0", - "@amplitude/node": "^1.9.0", "@coveo/cli-commons": "2.6.0", "@coveo/cli-plugin-source": "2.0.12", "@coveo/platform-client": "44.1.0", @@ -40,6 +40,7 @@ "tslib": "2.5.0" }, "devDependencies": { + "@amplitude/analytics-types": "^2.1.2", "@amplitude/types": "1.10.2", "@babel/core": "7.21.5", "@coveo/angular": "1.36.0", diff --git a/packages/cli/core/src/hooks/analytics/amplitudeClient.ts b/packages/cli/core/src/hooks/analytics/amplitudeClient.ts deleted file mode 100644 index 55e9007647..0000000000 --- a/packages/cli/core/src/hooks/analytics/amplitudeClient.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {init, NodeClient} from '@amplitude/node'; - -const analyticsAPIKey = 'af28cba7acfd392c324bebd399e2d9ea'; - -export interface AmplitudeClient extends NodeClient { - identified: boolean; -} - -// TODO: CDX-667: support proxy -export const amplitudeClient = init(analyticsAPIKey); diff --git a/packages/cli/core/src/hooks/analytics/analytics.spec.ts b/packages/cli/core/src/hooks/analytics/analytics.spec.ts deleted file mode 100644 index 2cb0527f80..0000000000 --- a/packages/cli/core/src/hooks/analytics/analytics.spec.ts +++ /dev/null @@ -1,239 +0,0 @@ -jest.mock('jsonschema'); -jest.mock('@amplitude/node'); -jest.mock('@coveo/cli-commons/config/config'); -jest.mock('@coveo/cli-commons/platform/authenticatedClient'); -jest.mock('@coveo/platform-client'); -jest.mock('@coveo/cli-commons/config/globalConfig'); - -import {Config} from '@coveo/cli-commons/config/config'; -import { - AuthenticatedClient, - AuthenticationStatus, - getAuthenticationStatus, -} from '@coveo/cli-commons/platform/authenticatedClient'; -import hook, {AnalyticsHook} from './analytics'; -import {Interfaces} from '@oclif/core'; -import {PlatformClient} from '@coveo/platform-client'; -import {configurationMock} from '../../__stub__/configuration'; -import {fancyIt} from '@coveo/cli-commons-dev/testUtils/it'; -import globalConfig from '@coveo/cli-commons/config/globalConfig'; -const mockedGlobalConfig = jest.mocked(globalConfig); -const mockedConfig = jest.mocked(Config); -const mockedPlatformClient = jest.mocked(PlatformClient); -const mockedAuthenticatedClient = jest.mocked(AuthenticatedClient); -const mockedAuthenticationStatus = jest.mocked(getAuthenticationStatus); -const mockedLogEvent = jest.fn(); - -jest.mock('./amplitudeClient', () => ({ - get amplitudeClient() { - return { - logEvent: mockedLogEvent, - }; - }, -})); -describe('analytics_hook', () => { - const mockedUserGet = jest.fn(); - const mockedLicense = jest.fn(); - - const getAnalyticsHook = (input: Partial): AnalyticsHook => { - return { - event: { - event_type: 'started foo bar', - event_properties: { - key: 'value', - }, - }, - config: {} as Interfaces.Config, - ...input, - }; - }; - - const doMockPlatformClient = () => { - mockedUserGet.mockResolvedValue({ - email: 'bob@coveo.com', - username: 'bob@coveo.com', - displayName: 'bob', - }); - mockedLicense.mockReturnValue({productType: 'TRIAL'}); - mockedPlatformClient.mockImplementation( - () => - ({ - initialize: () => Promise.resolve(), - user: { - get: mockedUserGet, - }, - license: { - full: mockedLicense, - }, - organization: { - get: jest.fn().mockResolvedValue({type: 'Production'}), - list: jest.fn().mockResolvedValue([{id: 'someorgid'}]), - }, - } as unknown as PlatformClient) - ); - }; - - const doMockConfiguration = () => { - mockedConfig.mockImplementation(configurationMock()); - }; - - const doMockAuthenticatedClient = () => { - mockedAuthenticatedClient.mockImplementation( - () => - ({ - getClient: () => - Promise.resolve( - mockedPlatformClient.getMockImplementation()!({ - accessToken: 'foo', - organizationId: 'bar', - }) - ), - cfg: mockedConfig.getMockImplementation()!('./'), - } as AuthenticatedClient) - ); - mockedAuthenticationStatus.mockImplementation(() => - Promise.resolve(AuthenticationStatus.LOGGED_IN) - ); - }; - - beforeAll(() => { - mockedGlobalConfig.get.mockReturnValue({ - configDir: 'the_config_dir', - } as Interfaces.Config); - }); - - beforeEach(() => { - doMockPlatformClient(); - doMockConfiguration(); - doMockAuthenticatedClient(); - }); - - afterEach(() => { - mockedLicense.mockClear(); - mockedPlatformClient.mockClear(); - mockedConfig.mockClear(); - mockedAuthenticatedClient.mockClear(); - }); - - fancyIt()('should log one event', async () => { - await hook(getAnalyticsHook({})); - expect(mockedLogEvent).toHaveBeenCalledTimes(1); - }); - - fancyIt()('should log event type and default properties', async () => { - await hook(getAnalyticsHook({})); - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({ - event_properties: { - environment: 'dev', - key: 'value', - organization_type: 'Production', - region: 'us', - }, - event_type: 'started foo bar', - }) - ); - }); - - describe('when identify option is set to false', () => { - beforeEach(async () => { - await hook(getAnalyticsHook({})); - }); - fancyIt()('should only log one event', () => { - expect(mockedLogEvent).toHaveBeenCalledTimes(1); - }); - - fancyIt()('should not log any identify event', () => { - expect(mockedLogEvent).toHaveReturnedWith( - expect.not.objectContaining({event_type: '$identify'}) - ); - }); - }); - - describe('when identify option is set to true', () => { - beforeEach(async () => { - await hook(getAnalyticsHook({identify: true})); - }); - - fancyIt()('should log 2 events', () => { - expect(mockedLogEvent).toHaveBeenCalledTimes(2); - }); - - fancyIt()('should log an identify event', () => { - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({event_type: '$identify'}) - ); - }); - - describe('when the user is a coveo employee', () => { - fancyIt()('should identify event with (un-hashed) email', () => { - const userIdCheck = expect.stringMatching('bob@coveo.com'); - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({user_id: userIdCheck}) - ); - }); - }); - - describe('when the user is not a coveo employee', () => { - beforeEach(async () => { - mockedUserGet.mockResolvedValueOnce({ - email: 'bob@acme.com', - username: 'bob@acme.com', - displayName: 'bob', - }); - mockedLogEvent.mockReset(); - await hook(getAnalyticsHook({identify: true})); - }); - - fancyIt()('should not identify event with (un-hashed) email', () => { - const userIdCheck = expect.not.stringMatching('bob@coveo.com'); - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({user_id: userIdCheck}) - ); - }); - }); - - fancyIt()('should identify event with device ID', () => { - const deviceIdCheck = expect.stringMatching(/.*/); - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({device_id: deviceIdCheck}) - ); - }); - }); - - fancyIt()('should identify the event', async () => { - await hook(getAnalyticsHook({})); - const nonEmptyString = expect.stringMatching(/.+/); - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({ - device_id: nonEmptyString, - user_id: nonEmptyString, - }) - ); - }); - - fancyIt()('should send analytics regardless of the license', async () => { - mockedLicense.mockResolvedValue({productType: 'ANYTHING_BUT_TRIAL'}); - - await hook(getAnalyticsHook({})); - expect(mockedLogEvent).toHaveBeenCalled(); - }); - - fancyIt()( - 'should not throw an error when the user is not logged in', - async () => { - mockedAuthenticationStatus.mockImplementationOnce(() => - Promise.resolve(AuthenticationStatus.LOGGED_OUT) - ); - - await expect(hook(getAnalyticsHook({}))).resolves.not.toThrow(); - } - ); - - fancyIt()('should not throw an error when the user is expired', async () => { - mockedAuthenticationStatus.mockImplementationOnce(() => - Promise.resolve(AuthenticationStatus.EXPIRED) - ); - await expect(hook(getAnalyticsHook({}))).resolves.not.toThrow(); - }); -}); diff --git a/packages/cli/core/src/hooks/analytics/analytics.ts b/packages/cli/core/src/hooks/analytics/analytics.ts index 84305299c3..5e6df3332c 100644 --- a/packages/cli/core/src/hooks/analytics/analytics.ts +++ b/packages/cli/core/src/hooks/analytics/analytics.ts @@ -1,12 +1,12 @@ -import {Event} from '@amplitude/node'; +import type {Event} from '@amplitude/analytics-types'; import {Interfaces} from '@oclif/core'; import { AuthenticatedClient, AuthenticationStatus, getAuthenticationStatus, } from '@coveo/cli-commons/platform/authenticatedClient'; -import {amplitudeClient} from './amplitudeClient'; -import {Identifier} from './identifier'; +import {track} from '@coveo/cli-commons/analytics/amplitudeClient'; +import {Identifier} from '@coveo/cli-commons/analytics/identifier'; export interface AnalyticsHook { event: Event; @@ -23,11 +23,11 @@ const hook = async function (options: AnalyticsHook) { const {userId, deviceId, identify} = await new Identifier().getIdentity(); if (options.identify) { - identify(amplitudeClient); + identify(); } await augmentEvent(options.event, platformIdentifier); - amplitudeClient.logEvent({ + track({ device_id: deviceId, ...(userId && {user_id: userId}), ...options.event, diff --git a/packages/cli/core/src/hooks/analytics/identifier.spec.ts b/packages/cli/core/src/hooks/analytics/identifier.spec.ts deleted file mode 100644 index 96dd2e45c2..0000000000 --- a/packages/cli/core/src/hooks/analytics/identifier.spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -jest.mock('@amplitude/node'); -jest.mock('@amplitude/identify'); -jest.mock('@coveo/platform-client'); -jest.mock('@coveo/cli-commons/platform/authenticatedClient'); -jest.mock('@coveo/cli-commons/config/config'); -jest.mock('@coveo/cli-commons/config/globalConfig'); - -import os from 'os'; -import {Identify} from '@amplitude/identify'; -import {Config, Configuration} from '@coveo/cli-commons/config/config'; -import {AuthenticatedClient} from '@coveo/cli-commons/platform/authenticatedClient'; -import {Identifier} from './identifier'; -import PlatformClient from '@coveo/platform-client'; -import { - configurationMock, - defaultConfiguration, -} from '../../__stub__/configuration'; -import type {Interfaces} from '@oclif/core'; -import type {NodeClient} from '@amplitude/node'; -import globalConfig from '@coveo/cli-commons/config/globalConfig'; - -describe('identifier', () => { - const mockedGlobalConfig = jest.mocked(globalConfig); - const mockedConfig = jest.mocked(Config); - const mockedIdentify = jest.mocked(Identify); - const mockedAuthenticatedClient = jest.mocked(AuthenticatedClient); - const mockedPlatformClient = jest.mocked(PlatformClient); - const mockUserGet = jest.fn(); - const mockSetIdentity = jest.fn(); - const mockedLogEvent = jest.fn(); - const mockedOsVersion = jest.spyOn(os, 'release'); - - let identity: Awaited>; - - const getDummyAmplitudeClient = () => - ({ - logEvent: mockedLogEvent, - } as unknown as NodeClient); - - const doMockOS = () => { - mockedOsVersion.mockReturnValue('21.3.4'); - }; - const doMockIdentify = () => { - mockedIdentify.prototype.set.mockImplementation(mockSetIdentity); - }; - const doMockPlatformClient = (email = '') => { - mockedPlatformClient.mockImplementation( - () => - ({ - initialize: () => Promise.resolve(), - user: { - get: mockUserGet.mockResolvedValue({ - email, - }), - }, - organization: { - get: jest.fn().mockResolvedValue({type: 'Production'}), - }, - } as unknown as PlatformClient) - ); - }; - const doMockConfiguration = ( - overrideConfiguration?: Partial - ) => { - mockedConfig.mockImplementation( - configurationMock({...defaultConfiguration, ...overrideConfiguration}) - ); - }; - const doMockAuthenticatedClient = () => { - mockedAuthenticatedClient.mockImplementation( - () => - ({ - getClient: () => - Promise.resolve( - mockedPlatformClient.getMockImplementation()!({ - accessToken: 'foo', - organizationId: 'bar', - }) - ), - cfg: mockedConfig.getMockImplementation()!('./'), - } as unknown as AuthenticatedClient) - ); - }; - - const mockForInternalUser = () => { - doMockConfiguration(); - doMockPlatformClient('bob@coveo.com'); - }; - const mockForExternalUser = () => { - doMockConfiguration(); - doMockPlatformClient('bob@acme.com'); - }; - const mockForAnonymousUser = () => { - doMockConfiguration({anonymous: true}); - doMockPlatformClient(); - }; - - beforeAll(() => { - mockedGlobalConfig.get.mockReturnValue({ - configDir: 'the_config_dir', - version: '1.2.3', - platform: 'darwin', - } as Interfaces.Config); - }); - - beforeEach(() => { - doMockOS(); - doMockIdentify(); - doMockAuthenticatedClient(); - }); - - describe('when the user is internal', () => { - beforeEach(async () => { - mockForInternalUser(); - identity = await new Identifier().getIdentity(); - }); - - afterEach(() => { - mockUserGet.mockClear(); - mockedPlatformClient.mockClear(); - }); - - it('should not set platform information', () => { - expect(mockSetIdentity).not.toHaveBeenCalledWith( - 'organization_type', - 'Production' - ); - expect(mockSetIdentity).not.toHaveBeenCalledWith('environment', 'dev'); - expect(mockSetIdentity).not.toHaveBeenCalledWith('region', 'us'); - }); - - it('should set the user ID (unhashed)', () => { - expect(identity.userId).toBe('bob@coveo.com'); - }); - - it('should set is_internal_user to true', () => { - expect(mockSetIdentity).toHaveBeenCalledWith('is_internal_user', true); - }); - - it('should identify event with (un-hashed) email', () => { - expect(identity.userId).toBe('bob@coveo.com'); - }); - - it('should always identify events with a device ID', () => { - expect(identity.deviceId).toBeDefined(); - }); - }); - - describe('when the user is external', () => { - beforeEach(async () => { - mockForExternalUser(); - identity = await new Identifier().getIdentity(); - }); - - it('should set the user ID (hashed)', () => { - expect(identity.userId).not.toBeNull(); - expect(identity.userId).not.toBe('bob@acme.com'); - }); - - it('should set is_internal_user to false', () => { - expect(mockSetIdentity).toHaveBeenCalledWith('is_internal_user', false); - }); - }); - - describe('when the user is anonymous', () => { - beforeEach(async () => { - mockForAnonymousUser(); - identity = await new Identifier().getIdentity(); - }); - - it('should set the user ID', () => { - expect(identity.userId).not.toBeNull(); - }); - }); - - describe('when logging for every user type', () => { - beforeEach(async () => { - identity = await new Identifier().getIdentity(); - identity.identify(getDummyAmplitudeClient()); - }); - - it('should add the CLI version to the event', () => { - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({app_version: '1.2.3'}) - ); - }); - - it('should add the OS information to the event', () => { - expect(mockedLogEvent).toHaveBeenCalledWith( - expect.objectContaining({ - app_version: '1.2.3', - os_name: 'darwin', - os_version: '21.3.4', - platform: 'darwin', - }) - ); - }); - }); -}); diff --git a/packages/cli/core/src/hooks/analytics/identifier.ts b/packages/cli/core/src/hooks/analytics/identifier.ts deleted file mode 100644 index f7dccba2bd..0000000000 --- a/packages/cli/core/src/hooks/analytics/identifier.ts +++ /dev/null @@ -1,123 +0,0 @@ -import os from 'os'; -import {Identify} from '@amplitude/identify'; -import {machineId} from 'node-machine-id'; -import {createHash} from 'crypto'; -import {AuthenticatedClient} from '@coveo/cli-commons/platform/authenticatedClient'; -import PlatformClient from '@coveo/platform-client'; -import {camelToSnakeCase} from '@coveo/cli-commons/utils/string'; -import type {NodeClient} from '@amplitude/node'; -import globalConfig from '@coveo/cli-commons/config/globalConfig'; -import {Configuration} from '@coveo/cli-commons/config/config'; - -export class Identifier { - private authenticatedClient: AuthenticatedClient; - - public constructor() { - this.authenticatedClient = new AuthenticatedClient(); - } - - public async getIdentity() { - const platformClient = await this.authenticatedClient.getClient(); - await platformClient.initialize(); - - const identifier = new Identify(); - const {userId, isInternalUser} = await this.getAnalyticsInfo( - platformClient - ); - const deviceId = await machineId(); - const identity = { - ...this.getShellInfo(), - ...this.getDeviceInfo(), - ...{isInternalUser}, - }; - - Object.entries(identity).forEach(([key, value]) => { - identifier.set(camelToSnakeCase(key), value); - }); - - const identify = (amplitudeClient: NodeClient) => { - const identifyEvent = { - ...identifier.identifyUser(userId, deviceId), - ...this.getAmplitudeBaseEventProperties(), - ...this.getOrganizationIdentifier(), - }; - amplitudeClient.logEvent(identifyEvent); - }; - - return {userId, deviceId, identify}; - } - - private getOrganizationIdentifier() { - const {environment, region, organization} = this.configuration; - return {environment, region, organization}; - } - - private hash(word: string) { - const hash = createHash('sha256').update(word); - return hash.digest('hex').toString(); - } - - private getAnalyticsInfo(platformClient: PlatformClient) { - return this.configuration.anonymous - ? this.getApiKeyInfo() - : this.getUserInfo(platformClient); - } - - private getApiKeyInfo() { - const identifier = this.configuration.accessToken - ?.split('-') - .pop() as string; - return { - userId: this.hash(identifier), - isInternalUser: false, - }; - } - - private async getUserInfo(platformClient: PlatformClient) { - const {email} = await platformClient.user.get(); - const isInternalUser = email.match(/@coveo\.com$/) !== null; - - return { - userId: isInternalUser ? email : this.hash(email), - isInternalUser, - }; - } - - private getAmplitudeBaseEventProperties() { - const {version, platform} = globalConfig.get(); - return { - app_version: version, - os_version: os.release(), - os_name: platform, - platform, - }; - } - - private getDeviceInfo() { - const {arch, windows, bin, userAgent, debug} = globalConfig.get(); - return { - arch, - windows, - bin, - userAgent, - debug, - }; - } - - private getShellInfo() { - const {shell} = globalConfig.get(); - const { - TERM_PROGRAM_VERSION: termProgramVersion, - TERM_PROGRAM: termProgram, - } = process.env; - return { - shell, - ...(termProgramVersion && {termProgramVersion}), - ...(termProgram && {termProgram}), - }; - } - - private get configuration(): Configuration { - return this.authenticatedClient.cfg.get(); - } -} diff --git a/packages/cli/core/src/hooks/init/termination-signals.ts b/packages/cli/core/src/hooks/init/termination-signals.ts index 8a304ffa9d..6d91b5bbb7 100644 --- a/packages/cli/core/src/hooks/init/termination-signals.ts +++ b/packages/cli/core/src/hooks/init/termination-signals.ts @@ -7,7 +7,7 @@ export function handleTerminationSignals() { exitSignals.forEach((signal) => process.on(signal, async (_sig: string, exitCode: number) => { await trackEvent(signal); - await flush(); + flush(); /** * Signal Exits: If Node.js receives a fatal signal such as SIGTERM or SIGHUP, then its exit code will be 128 plus the value of the signal code. * This is a standard POSIX practice, since exit codes are defined to be 7-bit integers, and signal exits set the high-order bit, diff --git a/packages/cli/core/src/hooks/postrun/postrun.ts b/packages/cli/core/src/hooks/postrun/postrun.ts index 7d3b827dde..8e80017d39 100644 --- a/packages/cli/core/src/hooks/postrun/postrun.ts +++ b/packages/cli/core/src/hooks/postrun/postrun.ts @@ -1,8 +1,9 @@ import {Hook} from '@oclif/core'; import {flush} from '@coveo/cli-commons/analytics/amplitudeClient'; -const hook: Hook<'postrun'> = async function (_options) { - await flush(); +const hook: Hook<'postrun'> = function (_options) { + flush(); + return Promise.resolve(); }; export default hook;