feat: Setup posthog analytics (#12291)

- Replace June.so analytics with PostHog integration
- Maintain existing analytics API interface for seamless migration
- Remove all the June references

_June.so is shutting down their service, requiring migration to an
alternative analytics provider. PostHog was chosen as the replacement
due to its robust feature set and similar API structure._
This commit is contained in:
Muhsin Keloth
2025-08-27 09:10:06 +05:30
committed by GitHub
parent ad90deb709
commit 068538e3d6
5 changed files with 92 additions and 1363 deletions

View File

@@ -1,4 +1,4 @@
import { AnalyticsBrowser } from '@june-so/analytics-next'; import posthog from 'posthog-js';
/** /**
* AnalyticsHelper class to initialize and track user analytics * AnalyticsHelper class to initialize and track user analytics
@@ -26,10 +26,12 @@ export class AnalyticsHelper {
return; return;
} }
let [analytics] = await AnalyticsBrowser.load({ posthog.init(this.analyticsToken, {
writeKey: this.analyticsToken, api_host: 'https://app.posthog.com',
capture_pageview: false,
persistence: 'localStorage+cookie',
}); });
this.analytics = analytics; this.analytics = posthog;
} }
/** /**
@@ -43,8 +45,7 @@ export class AnalyticsHelper {
} }
this.user = user; this.user = user;
this.analytics.identify(this.user.email, { this.analytics.identify(this.user.id.toString(), {
userId: this.user.id,
email: this.user.email, email: this.user.email,
name: this.user.name, name: this.user.name,
avatar: this.user.avatar_url, avatar: this.user.avatar_url,
@@ -55,7 +56,7 @@ export class AnalyticsHelper {
account => account.id === accountId account => account.id === accountId
); );
if (currentAccount) { if (currentAccount) {
this.analytics.group(currentAccount.id, this.user.id, { this.analytics.group('company', currentAccount.id.toString(), {
name: currentAccount.name, name: currentAccount.name,
}); });
} }
@@ -71,12 +72,7 @@ export class AnalyticsHelper {
if (!this.analytics) { if (!this.analytics) {
return; return;
} }
this.analytics.capture(eventName, properties);
this.analytics.track({
userId: this.user.id,
event: eventName,
properties,
});
} }
/** /**
@@ -89,9 +85,9 @@ export class AnalyticsHelper {
return; return;
} }
this.analytics.page(params); this.analytics.capture('$pageview', params);
} }
} }
// This object is shared across, the init is called in app/javascript/packs/application.js // This object is shared across, the init is called in app/javascript/entrypoints/dashboard.js
export default new AnalyticsHelper(window.analyticsConfig); export default new AnalyticsHelper(window.analyticsConfig);

View File

@@ -1,15 +1,11 @@
import helperObject, { AnalyticsHelper } from '../'; import helperObject, { AnalyticsHelper } from '../';
vi.mock('@june-so/analytics-next', () => ({ vi.mock('posthog-js', () => ({
AnalyticsBrowser: { default: {
load: () => [ init: vi.fn(),
{ identify: vi.fn(),
identify: vi.fn(), capture: vi.fn(),
track: vi.fn(), group: vi.fn(),
page: vi.fn(),
group: vi.fn(),
},
],
}, },
})); }));
@@ -26,12 +22,12 @@ describe('AnalyticsHelper', () => {
}); });
describe('init', () => { describe('init', () => {
it('should initialize the analytics browser with the correct token', async () => { it('should initialize posthog with the correct token', async () => {
await analyticsHelper.init(); await analyticsHelper.init();
expect(analyticsHelper.analytics).not.toBe(null); expect(analyticsHelper.analytics).not.toBe(null);
}); });
it('should not initialize the analytics browser if token is not provided', async () => { it('should not initialize posthog if token is not provided', async () => {
analyticsHelper = new AnalyticsHelper(); analyticsHelper = new AnalyticsHelper();
await analyticsHelper.init(); await analyticsHelper.init();
expect(analyticsHelper.analytics).toBe(null); expect(analyticsHelper.analytics).toBe(null);
@@ -43,36 +39,36 @@ describe('AnalyticsHelper', () => {
analyticsHelper.analytics = { identify: vi.fn(), group: vi.fn() }; analyticsHelper.analytics = { identify: vi.fn(), group: vi.fn() };
}); });
it('should call identify on analytics browser with correct arguments', () => { it('should call identify on posthog with correct arguments', () => {
analyticsHelper.identify({ analyticsHelper.identify({
id: '123', id: 123,
email: 'test@example.com', email: 'test@example.com',
name: 'Test User', name: 'Test User',
avatar_url: 'avatar_url', avatar_url: 'avatar_url',
accounts: [{ id: '1', name: 'Account 1' }], accounts: [{ id: 1, name: 'Account 1' }],
account_id: '1', account_id: 1,
}); });
expect(analyticsHelper.analytics.identify).toHaveBeenCalledWith( expect(analyticsHelper.analytics.identify).toHaveBeenCalledWith('123', {
'test@example.com', email: 'test@example.com',
{ name: 'Test User',
userId: '123', avatar: 'avatar_url',
email: 'test@example.com', });
name: 'Test User', expect(analyticsHelper.analytics.group).toHaveBeenCalledWith(
avatar: 'avatar_url', 'company',
} '1',
{ name: 'Account 1' }
); );
expect(analyticsHelper.analytics.group).toHaveBeenCalled();
}); });
it('should call identify on analytics browser without group', () => { it('should call identify on posthog without group', () => {
analyticsHelper.identify({ analyticsHelper.identify({
id: '123', id: 123,
email: 'test@example.com', email: 'test@example.com',
name: 'Test User', name: 'Test User',
avatar_url: 'avatar_url', avatar_url: 'avatar_url',
accounts: [{ id: '1', name: 'Account 1' }], accounts: [{ id: 1, name: 'Account 1' }],
account_id: '5', account_id: 5,
}); });
expect(analyticsHelper.analytics.group).not.toHaveBeenCalled(); expect(analyticsHelper.analytics.group).not.toHaveBeenCalled();
@@ -87,29 +83,27 @@ describe('AnalyticsHelper', () => {
describe('track', () => { describe('track', () => {
beforeEach(() => { beforeEach(() => {
analyticsHelper.analytics = { track: vi.fn() }; analyticsHelper.analytics = { capture: vi.fn() };
analyticsHelper.user = { id: '123' }; analyticsHelper.user = { id: 123 };
}); });
it('should call track on analytics browser with correct arguments', () => { it('should call capture on posthog with correct arguments', () => {
analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' }); analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' });
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith({ expect(analyticsHelper.analytics.capture).toHaveBeenCalledWith(
userId: '123', 'Test Event',
event: 'Test Event', { prop1: 'value1', prop2: 'value2' }
properties: { prop1: 'value1', prop2: 'value2' }, );
});
}); });
it('should call track on analytics browser with default properties', () => { it('should call capture on posthog with default properties', () => {
analyticsHelper.track('Test Event'); analyticsHelper.track('Test Event');
expect(analyticsHelper.analytics.track).toHaveBeenCalledWith({ expect(analyticsHelper.analytics.capture).toHaveBeenCalledWith(
userId: '123', 'Test Event',
event: 'Test Event', {}
properties: {}, );
});
}); });
it('should not call track on analytics browser if analytics is not initialized', () => { it('should not call capture on posthog if analytics is not initialized', () => {
analyticsHelper.analytics = null; analyticsHelper.analytics = null;
analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' }); analyticsHelper.track('Test Event', { prop1: 'value1', prop2: 'value2' });
expect(analyticsHelper.analytics).toBe(null); expect(analyticsHelper.analytics).toBe(null);
@@ -118,19 +112,22 @@ describe('AnalyticsHelper', () => {
describe('page', () => { describe('page', () => {
beforeEach(() => { beforeEach(() => {
analyticsHelper.analytics = { page: vi.fn() }; analyticsHelper.analytics = { capture: vi.fn() };
}); });
it('should call the analytics.page method with the correct arguments', () => { it('should call the capture method for pageview with the correct arguments', () => {
const params = { const params = {
name: 'Test page', name: 'Test page',
url: '/test', url: '/test',
}; };
analyticsHelper.page(params); analyticsHelper.page(params);
expect(analyticsHelper.analytics.page).toHaveBeenCalledWith(params); expect(analyticsHelper.analytics.capture).toHaveBeenCalledWith(
'$pageview',
params
);
}); });
it('should not call analytics.page if analytics is null', () => { it('should not call analytics.capture if analytics is null', () => {
analyticsHelper.analytics = null; analyticsHelper.analytics = null;
analyticsHelper.page(); analyticsHelper.page();
expect(analyticsHelper.analytics).toBe(null); expect(analyticsHelper.analytics).toBe(null);

View File

@@ -219,7 +219,7 @@
- name: ANALYTICS_TOKEN - name: ANALYTICS_TOKEN
value: value:
display_title: 'Analytics Token' display_title: 'Analytics Token'
description: 'The June.so analytics token for Chatwoot cloud' description: 'The PostHog analytics token for Chatwoot cloud'
type: secret type: secret
- name: CLEARBIT_API_KEY - name: CLEARBIT_API_KEY
value: value:

View File

@@ -40,7 +40,6 @@
"@hcaptcha/vue3-hcaptcha": "^1.3.0", "@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@highlightjs/vue-plugin": "^2.1.0", "@highlightjs/vue-plugin": "^2.1.0",
"@iconify-json/material-symbols": "^1.2.10", "@iconify-json/material-symbols": "^1.2.10",
"@june-so/analytics-next": "^2.0.0",
"@lk77/vue3-color": "^3.0.6", "@lk77/vue3-color": "^3.0.6",
"@radix-ui/colors": "^3.0.0", "@radix-ui/colors": "^3.0.0",
"@rails/actioncable": "6.1.3", "@rails/actioncable": "6.1.3",
@@ -80,6 +79,7 @@
"md5": "^2.3.0", "md5": "^2.3.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"opus-recorder": "^8.0.5", "opus-recorder": "^8.0.5",
"posthog-js": "^1.260.2",
"semver": "7.6.3", "semver": "7.6.3",
"snakecase-keys": "^8.0.1", "snakecase-keys": "^8.0.1",
"timezone-phone-codes": "^0.0.2", "timezone-phone-codes": "^0.0.2",

1332
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff