diff --git a/package-lock.json b/package-lock.json index 437992558..ac0bfbe74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "^12.2.1", "@angular/router": "^12.2.1", "@apollo/client": "^3.4.13", + "@fullstory/browser": "^1.4.9", "@hypertrace/hyperdash": "^1.2.1", "@hypertrace/hyperdash-angular": "^2.6.0", "@types/d3-hierarchy": "^2.0.0", @@ -47,6 +48,7 @@ "graphql-tag": "^2.12.5", "iso8601-duration": "^1.3.0", "lodash-es": "^4.17.21", + "mixpanel-browser": "^2.41.0", "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", @@ -3540,6 +3542,11 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "node_modules/@fullstory/browser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", + "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -20475,6 +20482,11 @@ "node": ">=0.10.0" } }, + "node_modules/mixpanel-browser": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", + "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -32526,6 +32538,11 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "@fullstory/browser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", + "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" + }, "@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -46026,6 +46043,11 @@ "is-extendable": "^1.0.1" } }, + "mixpanel-browser": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", + "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", diff --git a/package.json b/package.json index 85a4562f0..9fe73858d 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,9 @@ "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", - "zone.js": "~0.11.4" + "zone.js": "~0.11.4", + "@fullstory/browser": "^1.4.9", + "mixpanel-browser": "^2.41.0" }, "devDependencies": { "@angular-builders/jest": "^11.2.0", diff --git a/projects/common/package.json b/projects/common/package.json index e995da500..a05eaa6a0 100644 --- a/projects/common/package.json +++ b/projects/common/package.json @@ -20,7 +20,9 @@ "zone.js": "~0.11.4", "lodash-es": "^4.17.21", "d3-interpolate": "^2.0.1", - "d3-color": "^1.4.0" + "d3-color": "^1.4.0", + "@fullstory/browser": "^1.4.9", + "mixpanel-browser": "^2.41.0" }, "devDependencies": { "@hypertrace/test-utils": "^0.0.0" diff --git a/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts b/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts new file mode 100644 index 000000000..8e212581a --- /dev/null +++ b/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from '../../telemetry'; +import { Dictionary } from './../../../utilities/types/types'; +import { FreshPaint, loadFreshPaint } from './load-snippet'; + +@Injectable({ providedIn: 'root' }) +export class FreshPaintTelemetry + implements UserTelemetryProvider { + private freshPaint?: FreshPaint; + + public initialize(config: InitConfig): void { + this.freshPaint = loadFreshPaint(); + this.freshPaint.init(config.orgId); + } + + public identify(userTraits: UserTraits): void { + this.freshPaint?.identify(userTraits.email, userTraits); + this.freshPaint?.addEventProperties(userTraits); + } + + public trackEvent(name: string, properties: Dictionary): void { + this.freshPaint?.track(name, properties); + } + + public trackPage(name: string, eventData: Dictionary): void { + this.freshPaint?.page(name, name, eventData); + } + + public trackError(name: string, eventData: Dictionary): void { + this.freshPaint?.track(name, eventData); + } +} diff --git a/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.d.ts b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.d.ts new file mode 100644 index 000000000..d44b545ec --- /dev/null +++ b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.d.ts @@ -0,0 +1,16 @@ +export function loadFreshPaint(): FreshPaint; + +export interface FreshPaint { + init(orgId: string): void; + identify(uid?: string, userVars?: UserVars): void; + identify(userVars?: UserVars): void; + track(eventName: string, properties?: {}): void; + addEventProperties(userVars?: UserVars): void; + page(category?: string, name?: string, userVars?: UserVars): void; +} + +interface UserVars { + displayName?: string; + email?: string; + [key: string]: any; +} diff --git a/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.js b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.js new file mode 100644 index 000000000..59f89bedb --- /dev/null +++ b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.js @@ -0,0 +1,81 @@ +// tslint:disable + +export const loadFreshPaint = () => { + if (window.freshpaint) { + return window.freshpaint; + } + (function (c, a) { + if (!a.__SV) { + var b = window; + try { + var d, + m, + j, + k = b.location, + f = k.hash; + d = function (a, b) { + return (m = a.match(RegExp(b + '=([^&]*)'))) ? m[1] : null; + }; + f && + d(f, 'fpState') && + ((j = JSON.parse(decodeURIComponent(d(f, 'fpState')))), + 'fpeditor' === j.action && + (b.sessionStorage.setItem('_fpcehash', f), + history.replaceState(j.desiredHash || '', c.title, k.pathname + k.search))); + } catch (n) {} + var l, h; + window.freshpaint = a; + a._i = []; + a.init = function (b, d, g) { + function c(b, i) { + var a = i.split('.'); + 2 == a.length && ((b = b[a[0]]), (i = a[1])); + b[i] = function () { + b.push([i].concat(Array.prototype.slice.call(arguments, 0))); + }; + } + var e = a; + 'undefined' !== typeof g ? (e = a[g] = []) : (g = 'freshpaint'); + e.people = e.people || []; + e.toString = function (b) { + var a = 'freshpaint'; + 'freshpaint' !== g && (a += '.' + g); + b || (a += ' (stub)'); + return a; + }; + e.people.toString = function () { + return e.toString(1) + '.people (stub)'; + }; + l = 'disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove people group page alias ready addEventProperties addInitialEventProperties removeEventProperty addPageviewProperties'.split( + ' ' + ); + for (h = 0; h < l.length; h++) c(e, l[h]); + var f = 'set set_once union unset remove delete'.split(' '); + e.get_group = function () { + function a(c) { + b[c] = function () { + call2_args = arguments; + call2 = [c].concat(Array.prototype.slice.call(call2_args, 0)); + e.push([d, call2]); + }; + } + for (var b = {}, d = ['get_group'].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < f.length; c++) + a(f[c]); + return b; + }; + a._i.push([b, d, g]); + }; + a.__SV = 1.4; + b = c.createElement('script'); + b.type = 'text/javascript'; + b.async = !0; + b.src = + 'undefined' !== typeof FRESHPAINT_CUSTOM_LIB_URL + ? FRESHPAINT_CUSTOM_LIB_URL + : '//perfalytics.com/static/js/freshpaint.js'; + (d = c.getElementsByTagName('script')[0]) ? d.parentNode.insertBefore(b, d) : c.head.appendChild(b); + } + })(document, window.freshpaint || []); + + return freshpaint; +}; diff --git a/projects/common/src/telemetry/providers/fullstory/full-story-provider.ts b/projects/common/src/telemetry/providers/fullstory/full-story-provider.ts new file mode 100644 index 000000000..08d16f2de --- /dev/null +++ b/projects/common/src/telemetry/providers/fullstory/full-story-provider.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import * as FullStory from '@fullstory/browser'; +import { Dictionary } from '../../../utilities/types/types'; +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from './../../telemetry'; + +@Injectable({ providedIn: 'root' }) +export class FullStoryTelemetry + implements UserTelemetryProvider { + public initialize(config: InitConfig): void { + FullStory.init({ + orgId: config.orgId, + devMode: false + }); + } + + public identify(userTraits: UserTraits): void { + FullStory.setUserVars({ + displayName: userTraits.name ?? `${userTraits.givenName as string} ${userTraits.familyName as string}`, + email: userTraits.email, + companyName: userTraits.companyName, + licenseTier: userTraits.licenseTier, + licenseExpiration: userTraits.licenseExpiration, + isPlayground: userTraits.isPlayground, + accountEmail: userTraits.accountEmail + }); + } + + public trackEvent(name: string, eventData: Dictionary): void { + FullStory.event(name, eventData); + } + + public trackPage(name: string, eventData: Dictionary): void { + FullStory.event(name, eventData); + } + + public trackError(name: string, eventData: Dictionary): void { + FullStory.event(name, eventData); + } + + public shutdown(): void { + FullStory.shutdown(); + } +} diff --git a/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts b/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts new file mode 100644 index 000000000..9c005dbb1 --- /dev/null +++ b/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts @@ -0,0 +1,42 @@ +import { Dictionary } from './../../../utilities/types/types'; +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from '../../telemetry'; +import { loadGA } from './load-snippet'; + +export class GoogleAnalyticsTelemetry + implements UserTelemetryProvider { + public initialize(config: InitConfig): void { + const ga = loadGA(); + ga('create', config.orgId, 'auto'); + ga('send', 'pageview'); + } + + public identify(userTraits: UserTraits): void { + ga('set', 'userId', userTraits.email); + } + + public trackEvent(name: string, eventData: Dictionary): void { + ga('send', { + hitType: 'event', + eventCategory: 'user-actions', + eventAction: name, + ...eventData + }); + } + + public trackPage(name: string, eventData: Dictionary): void { + ga('send', { + hitType: 'pageview', + page: name, + ...eventData + }); + } + + public trackError(name: string, eventData: Dictionary): void { + ga('send', { + hitType: 'event', + eventCategory: 'error', + eventAction: name, + ...eventData + }); + } +} diff --git a/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.d.ts b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.d.ts new file mode 100644 index 000000000..3134f0de3 --- /dev/null +++ b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.d.ts @@ -0,0 +1,3 @@ +import 'google.analytics'; + +export function loadGA(): UniversalAnalytics.ga; diff --git a/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.js b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.js new file mode 100644 index 000000000..69a8b6f0f --- /dev/null +++ b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.js @@ -0,0 +1,51 @@ +// tslint:disable + +export const loadGA = () => { + /** + * Creates a temporary global ga object and loads analytics.js. + * Parameters o, a, and m are all used internally. They could have been + * declared using 'var', instead they are declared as parameters to save + * 4 bytes ('var '). + * + * @param {Window} i The global context object. + * @param {HTMLDocument} s The DOM document object. + * @param {string} o Must be 'script'. + * @param {string} g Protocol relative URL of the analytics.js script. + * @param {string} r Global name of analytics object. Defaults to 'ga'. + * @param {HTMLElement} a Async script tag. + * @param {HTMLElement} m First script tag in document. + */ + + if (window.ga) { + return window.ga; + } + + /** + * Do not modify. Below snippet is from Google + */ + + (function (i, s, o, g, r, a, m) { + i['GoogleAnalyticsObject'] = r; // Acts as a pointer to support renaming. + + // Creates an initial ga() function. + // The queued commands will be executed once analytics.js loads. + (i[r] = + i[r] || + function () { + (i[r].q = i[r].q || []).push(arguments); + }), + // Sets the time (as an integer) this tag was executed. + // Used for timing hits. + (i[r].l = 1 * new Date()); + + // Insert the script tag asynchronously. + // Inserts above current tag to prevent blocking in addition to using the + // async attribute. + (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m); + })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); + + return ga; +}; diff --git a/projects/common/src/telemetry/providers/mixpanel/mixpanel-provider.ts b/projects/common/src/telemetry/providers/mixpanel/mixpanel-provider.ts new file mode 100644 index 000000000..2fac7d2e4 --- /dev/null +++ b/projects/common/src/telemetry/providers/mixpanel/mixpanel-provider.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import mixpanel from 'mixpanel-browser'; +import { Dictionary } from '../../../utilities/types/types'; +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from '../../telemetry'; + +@Injectable({ providedIn: 'root' }) +export class MixPanelTelemetry + implements UserTelemetryProvider { + public initialize(config: InitConfig): void { + mixpanel.init(config.orgId); + } + + public identify(userTraits: UserTraits): void { + mixpanel.identify(userTraits.email); + mixpanel.people.set({ + displayName: userTraits.name ?? `${String(userTraits.givenName)} ${String(userTraits.familyName)}`, + email: userTraits.email, + companyName: userTraits.companyName, + licenseTier: userTraits.licenseTier, + licenseExpiration: userTraits.licenseExpiration, + isPlayground: userTraits.isPlayground, + accountEmail: userTraits.accountEmail + }); + } + + public trackEvent(name: string, eventData: Dictionary): void { + mixpanel.track(name, eventData); + } + + public trackPage(name: string, eventData: Dictionary): void { + mixpanel.track(name, eventData); + } + + public trackError(name: string, eventData: Dictionary): void { + mixpanel.track(name, eventData); + } + + public shutdown(): void { + mixpanel.disable(); + } +}