Skip to content

Commit

Permalink
Merge pull request #274 from atlassian/ARC-2825-be-analytics
Browse files Browse the repository at this point in the history
ARC-2825 be analytics
  • Loading branch information
joshkay10 authored Feb 14, 2024
2 parents 243ed2c + 2c5fe49 commit ab10fcf
Show file tree
Hide file tree
Showing 20 changed files with 1,822 additions and 941 deletions.
91 changes: 91 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"prepare": "cd ../ && husky install"
},
"dependencies": {
"@atlassiansox/analytics-node-client": "^4.1.1",
"@forge/api": "3.0.0",
"@forge/metrics": "0.2.0",
"@forge/resolver": "1.5.14",
Expand Down
40 changes: 40 additions & 0 deletions app/src/analytics/analytics-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isProductionEnv, createAnonymousId, getAccountDetails } from './analytics-client';

jest.mock('./analytics-client', () => ({
...jest.requireActual('./analytics-client'),
getAnalyticsClient: jest.fn().mockReturnValue({ cats: 'ssss' })
}));

jest.mock('@atlassiansox/analytics-node-client');

describe('isProductionEnv', () => {
test('should return true for production environment', () => {
process.env.NODE_ENV = 'prod';
expect(isProductionEnv()).toBe(true);
});
test('should return true for production environment', () => {
process.env.NODE_ENV = 'somethingelse';
expect(isProductionEnv()).toBe(false);
});
});
describe('createAnonymousId', () => {
test('should create a hashed anonymousId', () => {
const result = createAnonymousId('testInput');
expect(result).toBe('620ae460798e1f4cab44c44f3085620284f0960a276bbc3f0bd416449df14dbe');
});
});
describe('getAccountDetails', () => {
test('should return anonymousId when no accountId', () => {
const result = getAccountDetails('', 'ipAddress');
expect(result).toEqual({
anonymousId: '095cf3ff5683bd7c1db48018af7d0bee79205bcafb47a09b97f9faf0bcee4800'
});
});
test('should return accountId object', () => {
const result = getAccountDetails('testAccount');
expect(result).toEqual({
userIdType: 'atlassianAccount',
userId: 'testAccount',
});
});
});
91 changes: 91 additions & 0 deletions app/src/analytics/analytics-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import crypto from 'crypto';

interface AnalyticsAttributes {
[key: string]: any;
}

interface EventPayload {
eventName: string,
action: string,
actionSubject: string,
attributes?: AnalyticsAttributes
}

enum EnvType {
LOCAL = 'local',
DEV = 'dev',
STAGING = 'staging',
PROD = 'prod'
}

// eslint-disable-next-line max-len
export const sendAnalytics = async (cloudId: string, eventPayload: EventPayload, accountId?: string, anonymousId?: string): Promise<void> => {
sendEvent(cloudId, eventPayload, accountId, anonymousId)
.then(() => console.log('Analytics event processed'))
.catch((e) => console.error({ e }, 'Failed to send analytics event'));
};

// eslint-disable-next-line max-len
const sendEvent = async (cloudId: string, eventPayload: EventPayload, accountId?: string, anonymousId?: string): Promise<void> => {
const analyticsClient = await getAnalyticsClient();
const {
eventName, attributes, actionSubject, action
} = eventPayload;

if (!analyticsClient || !isProductionEnv()) {
console.warn('Analytics sendEvent skipped: @atlassiansox/analytics-node-client module not found or environment not production.');
return;
}

const accountDetails = getAccountDetails(accountId, anonymousId);

await analyticsClient.sendTrackEvent({
...accountDetails,
tenantIdType: 'cloudId',
tenantId: cloudId,
trackEvent: {
source: eventName,
action: action || 'unknown',
actionSubject: actionSubject || eventName,
attributes: {
...attributes
}
}
});
};

export const isProductionEnv = (): boolean => {
const env = (process.env.NODE_ENV || '').toLowerCase();
return ['production', 'prod'].includes(env);
};

export const getAccountDetails = (accountId?: string, anonymousId?: string) => {
if (accountId) {
return {
userIdType: 'atlassianAccount',
userId: accountId,
};
}
const hashedAnonymousId = createAnonymousId(anonymousId || 'default');
return { anonymousId: hashedAnonymousId };
};

export const createAnonymousId = (input: string): string => {
const hash = crypto.createHash('sha256').update(input).digest('hex');
return hash;
};

export const getAnalyticsClient = async (): Promise<any> => {
try {
const { analyticsClient } = await import('@atlassiansox/analytics-node-client');

const analyticsNodeClient = analyticsClient({
env: EnvType.PROD,
product: 'jenkinsForJira'
});

return analyticsNodeClient;
} catch (error) {
return null;
}
};
16 changes: 16 additions & 0 deletions app/src/analytics/analytics-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// All variables below were defined by DataPortal. Do not change their values
// as it will affect our metrics logs and dashboards.
export enum AnalyticsEventTypes {
ScreenEvent = 'screen', // user navigates to a particular screen, tab, drawer, modal, or inline-dialog
UiEvent = 'ui', // user interacts with a user interface element such as a button, text field, or link
TrackEvent = 'track', // user completes a product action e.g. submits form
OperationalEvent = 'operational' // help measure usages or performance of implementation detail
}

export enum AnalyticsTrackEventsEnum {
ConfigDataReceivedName = 'ConfigDataReceived',
DisconnectedServerName = 'DisconnectedServer',
ConnectionCreatedName = 'ConnectionCreated'
}

export enum AnalyticsOperationalEventsEnum {}
8 changes: 8 additions & 0 deletions app/src/analytics/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// All variables below were defined by DataPortal.
// Do not change their values as it will affect our metrics logs and dashboards.
export enum AnalyticsEventTypes {
ScreenEvent = 'screen', // user navigates to a particular screen, tab, drawer, modal, or inline-dialog
UiEvent = 'ui', // user interacts with a user interface element such as a button, text field, or link
TrackEvent = 'track', // user completes a product action e.g. submits form
OperationalEvent = 'operational' // help measure usages or performance of implementation detail
}
7 changes: 4 additions & 3 deletions app/src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ resolver.define('fetchJenkinsEventHandlerUrl', async (req) => {

resolver.define('connectJenkinsServer', async (req) => {
await adminPermissionCheck(req);
const { cloudId, accountId } = req.context;
const payload = req.payload as JenkinsServer;
internalMetrics.counter(metricResolverEmitter.connectJenkinsServer).incr();
return connectJenkinsServer(payload);
return connectJenkinsServer(payload, cloudId, accountId);
});

resolver.define('updateJenkinsServer', async (req) => {
Expand Down Expand Up @@ -66,11 +67,11 @@ resolver.define('getJenkinsServerWithSecret', async (req) => {

resolver.define('disconnectJenkinsServer', async (req) => {
await adminPermissionCheck(req);
const { cloudId } = req.context;
const { cloudId, accountId } = req.context;
const jenkinsServerUuid = req.payload.uuid;
internalMetrics.counter(metricResolverEmitter.disconnectJenkinsServer).incr();
return Promise.all([
disconnectJenkinsServer(jenkinsServerUuid),
disconnectJenkinsServer(jenkinsServerUuid, cloudId, accountId),
deleteBuilds(cloudId, jenkinsServerUuid),
deleteDeployments(cloudId, jenkinsServerUuid)
]);
Expand Down
2 changes: 1 addition & 1 deletion app/src/storage/connect-jenkins-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('Connect Jenkins Server Suite', () => {
secret: 'secret!!!',
pipelines: []
};
const result = await connectJenkinsServer(jenkinsServer);
const result = await connectJenkinsServer(jenkinsServer, 'cloudId', 'accountId');
expect(result).toBeTruthy();
});
});
17 changes: 15 additions & 2 deletions app/src/storage/connect-jenkins-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { JenkinsServer } from '../common/types';
import { SECRET_STORAGE_KEY_PREFIX, SERVER_STORAGE_KEY_PREFIX } from './constants';
import { JenkinsServerStorageError } from '../common/error';
import { Logger } from '../config/logger';
import { sendAnalytics } from '../analytics/analytics-client';
import { AnalyticsTrackEventsEnum } from '../analytics/analytics-events';

const connectJenkinsServer = async (jenkinsServer: JenkinsServer): Promise<boolean> => {
// eslint-disable-next-line max-len
const connectJenkinsServer = async (jenkinsServer: JenkinsServer, cloudId: string, accountId: string): Promise<boolean> => {
const logger = Logger.getInstance('connectJenkinsServer');

try {
Expand All @@ -15,12 +18,22 @@ const connectJenkinsServer = async (jenkinsServer: JenkinsServer): Promise<boole
await storage.setSecret(`${SECRET_STORAGE_KEY_PREFIX}${uuid}`, secret);

logger.info('Jenkins server configuration saved successfully!', { uuid });

await sendConnectAnalytics(cloudId, accountId);
return true;
} catch (error) {
logger.error('Failed to store Jenkins server configuration', { error });
throw new JenkinsServerStorageError('Failed to store jenkins server configuration');
}
};

const sendConnectAnalytics = async (cloudId: string, accountId: string) => {
const eventPayload = {
eventName: AnalyticsTrackEventsEnum.ConnectionCreatedName,
action: 'Connected new Jenkins server',
actionSubject: 'User action'
};

await sendAnalytics(cloudId, eventPayload, accountId);
};

export { connectJenkinsServer };
2 changes: 1 addition & 1 deletion app/src/storage/disconnect-jenkins-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { disconnectJenkinsServer } from './disconnect-jenkins-server';

describe('Delete Jenkins Server Suite', () => {
it('Should delete Jenkins server configuration from Forge Storage', async () => {
const result = await disconnectJenkinsServer('test-uid');
const result = await disconnectJenkinsServer('test-uid', 'cloudId', 'accountId');
expect(result).toBe(true);
});
});
Loading

0 comments on commit ab10fcf

Please sign in to comment.