[verified] privacy: remove product analytics
This commit is contained in:
@@ -17,7 +17,6 @@ class DashboardController < ActionController::Base
|
||||
CHATWOOT_INBOX_TOKEN
|
||||
API_CHANNEL_NAME
|
||||
API_CHANNEL_THUMBNAIL
|
||||
CLOUD_ANALYTICS_TOKEN
|
||||
DIRECT_UPLOADS_ENABLED
|
||||
MAXIMUM_FILE_UPLOAD_SIZE
|
||||
HCAPTCHA_SITE_KEY
|
||||
|
||||
@@ -1,98 +1,33 @@
|
||||
import * as amplitude from '@amplitude/analytics-browser';
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
/**
|
||||
* AnalyticsHelper class to initialize and track user analytics
|
||||
* @class AnalyticsHelper
|
||||
* Compatibility facade for dashboard analytics callsites.
|
||||
*
|
||||
* The community edition deliberately does not initialize a remote analytics
|
||||
* provider or retain user, account, event, or page data.
|
||||
*/
|
||||
export class AnalyticsHelper {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Object} [options={}] - options for analytics
|
||||
* @param {string} [options.token] - analytics token
|
||||
*/
|
||||
constructor({ token: analyticsToken } = {}) {
|
||||
this.analyticsToken = analyticsToken;
|
||||
constructor() {
|
||||
this.analytics = null;
|
||||
this.user = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize analytics
|
||||
* @function
|
||||
* @async
|
||||
*/
|
||||
async init() {
|
||||
if (!this.analyticsToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
amplitude.init(this.analyticsToken, {
|
||||
defaultTracking: false,
|
||||
});
|
||||
this.analytics = amplitude;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify the user
|
||||
* @function
|
||||
* @param {Object} user - User object
|
||||
*/
|
||||
identify(user) {
|
||||
if (!this.analytics || !user) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.user = user;
|
||||
this.analytics.setUserId(`user-${this.user.id.toString()}`);
|
||||
|
||||
const identifyEvent = new amplitude.Identify();
|
||||
identifyEvent.set('email', this.user.email);
|
||||
identifyEvent.set('name', this.user.name);
|
||||
identifyEvent.set('avatar', this.user.avatar_url);
|
||||
this.analytics.identify(identifyEvent);
|
||||
|
||||
const { accounts, account_id: accountId } = this.user;
|
||||
const [currentAccount] = accounts.filter(
|
||||
account => account.id === accountId
|
||||
);
|
||||
if (currentAccount) {
|
||||
const groupId = `account-${currentAccount.id.toString()}`;
|
||||
|
||||
this.analytics.setGroup('company', groupId);
|
||||
|
||||
const groupIdentify = new amplitude.Identify();
|
||||
groupIdentify.set('name', currentAccount.name);
|
||||
this.analytics.groupIdentify('company', groupId, groupIdentify);
|
||||
}
|
||||
identify() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track any event
|
||||
* @function
|
||||
* @param {string} eventName - event name
|
||||
* @param {Object} [properties={}] - event properties
|
||||
*/
|
||||
track(eventName, properties = {}) {
|
||||
if (!this.analytics) {
|
||||
return;
|
||||
}
|
||||
this.analytics.track(eventName, properties);
|
||||
track() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the page views
|
||||
* @function
|
||||
* @param {string} pageName - Page name
|
||||
* @param {Object} [properties={}] - Page view properties
|
||||
*/
|
||||
page(pageName, properties = {}) {
|
||||
if (!this.analytics) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.analytics.track('$pageview', { pageName, ...properties });
|
||||
page() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// This object is shared across, the init is called in app/javascript/entrypoints/dashboard.js
|
||||
export default new AnalyticsHelper(window.analyticsConfig);
|
||||
// Keep the shared object and method signatures for upstream compatibility.
|
||||
export default new AnalyticsHelper();
|
||||
/* eslint-enable class-methods-use-this */
|
||||
|
||||
@@ -1,144 +1,51 @@
|
||||
import helperObject, { AnalyticsHelper } from '../';
|
||||
|
||||
vi.mock('@amplitude/analytics-browser', () => ({
|
||||
init: vi.fn(),
|
||||
setUserId: vi.fn(),
|
||||
identify: vi.fn(),
|
||||
setGroup: vi.fn(),
|
||||
groupIdentify: vi.fn(),
|
||||
track: vi.fn(),
|
||||
Identify: vi.fn(() => ({
|
||||
set: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('helperObject', () => {
|
||||
it('should return an instance of AnalyticsHelper', () => {
|
||||
it('keeps the compatibility object as an AnalyticsHelper instance', () => {
|
||||
expect(helperObject).toBeInstanceOf(AnalyticsHelper);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AnalyticsHelper', () => {
|
||||
let analyticsHelper;
|
||||
|
||||
beforeEach(() => {
|
||||
analyticsHelper = new AnalyticsHelper({ token: 'test_token' });
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should initialize amplitude with the correct token', async () => {
|
||||
await analyticsHelper.init();
|
||||
expect(analyticsHelper.analytics).not.toBe(null);
|
||||
});
|
||||
it('never initializes a provider, even when a token is supplied', async () => {
|
||||
await expect(analyticsHelper.init()).resolves.toBeUndefined();
|
||||
|
||||
it('should not initialize amplitude if token is not provided', async () => {
|
||||
analyticsHelper = new AnalyticsHelper();
|
||||
await analyticsHelper.init();
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
});
|
||||
expect(analyticsHelper.analytics).toBeNull();
|
||||
});
|
||||
|
||||
describe('identify', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = {
|
||||
setUserId: vi.fn(),
|
||||
identify: vi.fn(),
|
||||
setGroup: vi.fn(),
|
||||
groupIdentify: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should call setUserId and identify on amplitude with correct arguments', () => {
|
||||
it('keeps identify as a no-op and does not retain user data', () => {
|
||||
expect(
|
||||
analyticsHelper.identify({
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
email: 'person@example.invalid',
|
||||
name: 'Test User',
|
||||
avatar_url: 'avatar_url',
|
||||
accounts: [{ id: 1, name: 'Account 1' }],
|
||||
accounts: [{ id: 1, name: 'Test Account' }],
|
||||
account_id: 1,
|
||||
});
|
||||
|
||||
expect(analyticsHelper.analytics.setUserId).toHaveBeenCalledWith(
|
||||
'user-123'
|
||||
);
|
||||
expect(analyticsHelper.analytics.identify).toHaveBeenCalled();
|
||||
expect(analyticsHelper.analytics.setGroup).toHaveBeenCalledWith(
|
||||
'company',
|
||||
'account-1'
|
||||
);
|
||||
expect(analyticsHelper.analytics.groupIdentify).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call identify on amplitude without group', () => {
|
||||
analyticsHelper.identify({
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatar_url: 'avatar_url',
|
||||
accounts: [{ id: 1, name: 'Account 1' }],
|
||||
account_id: 5,
|
||||
});
|
||||
|
||||
expect(analyticsHelper.analytics.setGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call analytics methods if analytics is null', () => {
|
||||
analyticsHelper.analytics = null;
|
||||
analyticsHelper.identify({});
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
});
|
||||
})
|
||||
).toBeUndefined();
|
||||
expect(analyticsHelper.user).toEqual({});
|
||||
});
|
||||
|
||||
describe('track', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { track: vi.fn() };
|
||||
analyticsHelper.user = { id: 123 };
|
||||
});
|
||||
|
||||
it('should call track on amplitude with correct arguments', () => {
|
||||
analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' });
|
||||
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith(
|
||||
'Test Event',
|
||||
{ prop1: 'value1', prop2: 'value2' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should call track on amplitude with default properties', () => {
|
||||
analyticsHelper.track('Test Event');
|
||||
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith(
|
||||
'Test Event',
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call track on amplitude if analytics is not initialized', () => {
|
||||
analyticsHelper.analytics = null;
|
||||
analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' });
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
});
|
||||
it('keeps track as a no-op and does not retain event data', () => {
|
||||
expect(
|
||||
analyticsHelper.track('Test Event', {
|
||||
email: 'person@example.invalid',
|
||||
account_id: 1,
|
||||
})
|
||||
).toBeUndefined();
|
||||
expect(analyticsHelper.analytics).toBeNull();
|
||||
});
|
||||
|
||||
describe('page', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { track: vi.fn() };
|
||||
});
|
||||
|
||||
it('should call the track method for pageview with the correct arguments', () => {
|
||||
const pageName = 'home';
|
||||
const properties = {
|
||||
path: '/test',
|
||||
name: 'home',
|
||||
};
|
||||
analyticsHelper.page(pageName, properties);
|
||||
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith(
|
||||
'$pageview',
|
||||
{ pageName: 'home', path: '/test', name: 'home' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call analytics.track if analytics is null', () => {
|
||||
analyticsHelper.analytics = null;
|
||||
analyticsHelper.page('home');
|
||||
expect(analyticsHelper.analytics).toBe(null);
|
||||
});
|
||||
it('keeps page as a no-op and does not retain page data', () => {
|
||||
expect(
|
||||
analyticsHelper.page('home', { path: '/dashboard' })
|
||||
).toBeUndefined();
|
||||
expect(analyticsHelper.analytics).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,13 +64,6 @@
|
||||
}
|
||||
window.errorLoggingConfig = '<%= ENV.fetch('SENTRY_FRONTEND_DSN', '') || ENV.fetch('SENTRY_DSN', '') %>'
|
||||
</script>
|
||||
<% if @global_config['CLOUD_ANALYTICS_TOKEN'].present? %>
|
||||
<script>
|
||||
window.analyticsConfig = {
|
||||
token: '<%= @global_config['CLOUD_ANALYTICS_TOKEN'] %>',
|
||||
}
|
||||
</script>
|
||||
<% end %>
|
||||
<%= vite_client_tag %>
|
||||
<%= vite_javascript_tag @application_pack %>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user