-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #280 from atlassian/NONE-test-analytics
analytics
- Loading branch information
Showing
11 changed files
with
2,666 additions
and
2,747 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,91 +1,168 @@ | ||
import crypto from 'crypto'; | ||
import fetch from 'node-fetch'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
|
||
interface AnalyticsAttributes { | ||
[key: string]: any; | ||
[key: string]: any; | ||
} | ||
|
||
interface EventPayload { | ||
eventName: string, | ||
action: string, | ||
actionSubject: string, | ||
attributes?: AnalyticsAttributes | ||
interface Metadata { | ||
nodeVersion: string; | ||
version: string; | ||
} | ||
|
||
interface Context { | ||
library: any | ||
} | ||
|
||
enum EnvType { | ||
LOCAL = 'local', | ||
DEV = 'dev', | ||
STAGING = 'staging', | ||
PROD = 'prod' | ||
interface EventPayload { | ||
action: string, | ||
actionSubject: string, | ||
attributes?: AnalyticsAttributes | ||
} | ||
|
||
interface Event { | ||
userId?: string; | ||
anonymousId: string; | ||
event: string; | ||
properties: { | ||
action: string; | ||
actionSubject: string; | ||
attributes: Record<string, string>; | ||
env: string; | ||
eventType: string; | ||
origin: string; | ||
product: string; | ||
tenantId: string; | ||
tenantIdType: string; | ||
source: string; | ||
userIdType?: string; | ||
}; | ||
type: string; | ||
timestamp: string; | ||
_metadata: Metadata; | ||
context: Context; | ||
messageId: string; | ||
} | ||
|
||
interface Payload { | ||
batch: Event[]; | ||
timestamp: string; | ||
sentAt: string; | ||
} | ||
|
||
interface Options { | ||
method: string; | ||
body: string; | ||
headers: { | ||
'Content-Type': string; | ||
}; | ||
} | ||
|
||
const environments = { | ||
LOCAL: 'local', | ||
DEV: 'dev', | ||
STAGING: 'stg', | ||
PROD: 'prod' | ||
}; | ||
|
||
const TRACK_EVENT_TYPE = 'track'; | ||
const PRODUCT = 'jenkinsForJira'; | ||
const ORIGIN = 'server'; | ||
const TENANT_ID_TYPE = 'cloudId'; | ||
|
||
// 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')); | ||
console.info('Analytics Request'); | ||
|
||
sendEvent(cloudId, eventPayload, accountId, anonymousId) | ||
.then(() => console.info('Analytics event processed')) | ||
.catch((e) => console.error({ e }, 'Failed to send analytics event')); | ||
}; | ||
|
||
// eslint-disable-next-line max-len | ||
const getAnalyticsEnvironmentUrl = () => { | ||
if (isProductionEnv()) { | ||
return process.env.ANALYTICS_URL; | ||
} | ||
return process.env.ANALYTICS_STAGE_URL; | ||
}; | ||
|
||
// eslint-disable-next-line max-len,@typescript-eslint/no-unused-vars | ||
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 | ||
} | ||
} | ||
}); | ||
const url = getAnalyticsEnvironmentUrl(); | ||
if (!url) { | ||
console.warn('No analytics path found.'); | ||
return; | ||
} | ||
const trackEvent: Event = createTrackEvent(cloudId, eventPayload, accountId, anonymousId); | ||
const timestamp = new Date().toISOString(); | ||
const payload: Payload = { | ||
batch: [trackEvent], | ||
timestamp, | ||
sentAt: timestamp | ||
}; | ||
|
||
const options: Options = { | ||
method: 'POST', | ||
body: JSON.stringify(payload), | ||
headers: { | ||
'Content-Type': 'application/json' | ||
} | ||
}; | ||
|
||
console.info('Sending Analytics'); | ||
|
||
await fetch(url, options); | ||
}; | ||
|
||
export const isProductionEnv = (): boolean => { | ||
const env = (process.env.NODE_ENV || '').toLowerCase(); | ||
return ['production', 'prod'].includes(env); | ||
// eslint-disable-next-line max-len | ||
export const createTrackEvent = (cloudId: string, eventPayload: EventPayload, accountId?: string, anonymousId?: string): Event => { | ||
const timestamp = new Date().toISOString(); | ||
const { | ||
attributes, actionSubject, action | ||
} = eventPayload; | ||
return { | ||
userId: accountId, | ||
anonymousId: createAnonymousId(anonymousId || 'default'), | ||
event: `${actionSubject} ${action}`, | ||
properties: { | ||
action, | ||
actionSubject, | ||
attributes: attributes || {}, | ||
product: PRODUCT, | ||
env: isProductionEnv() ? environments.PROD : environments.STAGING, | ||
eventType: TRACK_EVENT_TYPE, | ||
userIdType: accountId ? 'atlassianAccount' : undefined, | ||
tenantIdType: TENANT_ID_TYPE, | ||
tenantId: cloudId, | ||
source: 'server', | ||
origin: ORIGIN | ||
}, | ||
type: TRACK_EVENT_TYPE, | ||
timestamp, | ||
_metadata: { | ||
nodeVersion: process.versions.node, | ||
version: '0' | ||
}, | ||
context: { | ||
library: { | ||
name: 'analytics-node' | ||
} | ||
}, | ||
messageId: createMessageId() | ||
}; | ||
}; | ||
|
||
export const getAccountDetails = (accountId?: string, anonymousId?: string) => { | ||
if (accountId) { | ||
return { | ||
userIdType: 'atlassianAccount', | ||
userId: accountId, | ||
}; | ||
} | ||
const hashedAnonymousId = createAnonymousId(anonymousId || 'default'); | ||
return { anonymousId: hashedAnonymousId }; | ||
export const isProductionEnv = (): boolean => { | ||
const env = (process.env.NODE_ENV || '').toLowerCase(); | ||
return ['production', 'prod'].includes(env); | ||
}; | ||
|
||
export const createAnonymousId = (input: string): string => { | ||
const hash = crypto.createHash('sha256').update(input).digest('hex'); | ||
return hash; | ||
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; | ||
} | ||
export const createMessageId = () => { | ||
return `j4j-server-${uuidv4()}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.