Skip to content

Commit 1deae02

Browse files
[PostHog] implement "wallet | session start | Pageview" event (#606)
* refactor(extension): add post hog client context * feat(extension): add post hog experiments provider * refactor(extension): move posthog client to posthog provider folder * feat(extension): expose subscription to user tracking type changes * fix(extension): posthog tests * test(extension): add posthog client mock to broken test * refactor(extension): create subject for userTrackingType * refactor(extension): make postHogClientInstance static * test(extension): fix userIdService test * refactor(extension): move createInstance to PostHogClient * feat(extension): integrate experiment with password and name component * test(extension): fix broken tests * refactor(extension): improve naming and types * refactor(extension): move subscribeToDistinctIdUpdate to client constructor * test(extension): fix test for AnalyticsTracker * refactor(extension): remove comment and use Pick instead Partial * refactor(extension): remove local variant update * refactor(extension): load experiments on agree click * fix(extension): run experiment only for opted in user * test(extension): fix tests * feat(extension): postHog send start session event * fix(extension): check for cache on get variant call * fix(extension): address issue with analytics agree event * fix(extension): update userTrackingType on wallet base ID generation * feat(extension): postHog send start session event * feat(extension): logging start session PostHog * feat(extension): loggin start session posthog * feat(extension): send session start fix tests * feat(extension): post-hog-start-session LW-8343 * feat(extension): fix teset * feat(extension): fixing test * feat(extension): posthog fix test * feat(extension): fix-signing previous commits * Merge branch 'main' into feature/LW-8343-posthog-start-session-event --------- Co-authored-by: JP Lorek <juan.lorek@globant.com> Co-authored-by: jplorek-atix <65184652+jplorek-atix@users.noreply.github.com>
1 parent d959047 commit 1deae02

File tree

13 files changed

+115
-29
lines changed

13 files changed

+115
-29
lines changed

apps/browser-extension-wallet/src/lib/scripts/background/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ export const userIdServiceProperties: RemoteApiProperties<UserIdServiceInterface
4848
getRandomizedUserId: RemoteApiPropertyType.MethodReturningPromise,
4949
getUserId: RemoteApiPropertyType.MethodReturningPromise,
5050
userTrackingType$: RemoteApiPropertyType.HotObservable,
51+
isNewSession: RemoteApiPropertyType.MethodReturningPromise,
5152
resetToDefaultValues: RemoteApiPropertyType.MethodReturningPromise
5253
};

apps/browser-extension-wallet/src/lib/scripts/background/services/userIdService.test.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,27 @@ import { UserTrackingType } from '@providers/AnalyticsProvider/analyticsTracker'
77
const mockWalletBasedId =
88
'15d632f6b0ab82c72a194d634d8783ea0ef5419c8a8f638cb0c3fc49280e0a0285fc88fbfad04554779d19bec4ab30e5afee2f9ee736ba090c2213d98fe3a475';
99

10+
const mockedStorage = { state: {} };
1011
const generateStorageMocks = (
11-
store: Pick<BackgroundStorage, 'usePersistentUserId' | 'userId' | 'keyAgentsByChain'> = {}
12-
) => ({
13-
getStorageMock: jest.fn(() => Promise.resolve(store)),
14-
setStorageMock: jest.fn(),
15-
clearStorageMock: jest.fn()
16-
});
12+
state: Pick<BackgroundStorage, 'usePersistentUserId' | 'userId' | 'keyAgentsByChain'> = {}
13+
) => {
14+
mockedStorage.state = { ...state };
15+
return {
16+
getStorageMock: jest.fn(() => Promise.resolve(mockedStorage.state)),
17+
// setStorageMock: jest.fn(),
18+
setStorageMock: jest.fn((newState) => {
19+
mockedStorage.state = {
20+
...mockedStorage.state,
21+
...newState
22+
};
23+
return Promise.resolve();
24+
}),
25+
clearStorageMock: jest.fn(() => {
26+
mockedStorage.state = {};
27+
return Promise.resolve();
28+
})
29+
};
30+
};
1731

1832
describe('userIdService', () => {
1933
describe('restoring persistent user id', () => {
@@ -55,10 +69,9 @@ describe('userIdService', () => {
5569
userId: 'test'
5670
};
5771
const { getStorageMock, setStorageMock } = generateStorageMocks(store);
58-
5972
const userIdService = new UserIdService(getStorageMock, setStorageMock);
73+
await userIdService.init();
6074
await userIdService.makeTemporary();
61-
6275
expect(setStorageMock).toHaveBeenCalledWith({
6376
usePersistentUserId: false,
6477
userId: undefined
@@ -83,6 +96,7 @@ describe('userIdService', () => {
8396
const { getStorageMock, setStorageMock } = generateStorageMocks(store);
8497

8598
const userIdService = new UserIdService(getStorageMock, setStorageMock);
99+
await userIdService.init();
86100
await userIdService.makeTemporary();
87101

88102
// simulate an almost session timeout

apps/browser-extension-wallet/src/lib/scripts/background/services/userIdService.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class UserIdService implements UserIdServiceInterface {
2525
private sessionTimeout?: NodeJS.Timeout;
2626
private userIdRestored = false;
2727
public userTrackingType$ = new BehaviorSubject<UserTrackingType>(UserTrackingType.Basic);
28+
private hasNewSessionStarted = false;
2829

2930
constructor(
3031
private getStorage: typeof getBackgroundStorage = getBackgroundStorage,
@@ -33,6 +34,18 @@ export class UserIdService implements UserIdServiceInterface {
3334
private sessionLength: number = SESSION_LENGTH
3435
) {}
3536

37+
async init(): Promise<void> {
38+
if (!this.userIdRestored) {
39+
console.debug('[ANALYTICS] Restoring user ID...');
40+
await this.restoreUserId();
41+
}
42+
43+
if (!this.randomizedUserId) {
44+
console.debug('[ANALYTICS] User ID not found - generating new one');
45+
this.randomizedUserId = randomBytes(USER_ID_BYTE_SIZE).toString('hex');
46+
}
47+
}
48+
3649
private async getWalletBasedUserId(networkMagic: Wallet.Cardano.NetworkMagic): Promise<string | undefined> {
3750
const { keyAgentsByChain, usePersistentUserId } = await this.getStorage();
3851

@@ -59,16 +72,9 @@ export class UserIdService implements UserIdServiceInterface {
5972
return this.walletBasedUserId;
6073
}
6174

75+
// TODO: make this method private when Motamo is not longer in use
6276
async getRandomizedUserId(): Promise<string> {
63-
if (!this.userIdRestored) {
64-
console.debug('[ANALYTICS] Restoring user ID...');
65-
await this.restoreUserId();
66-
}
67-
68-
if (!this.randomizedUserId) {
69-
console.debug('[ANALYTICS] User ID not found - generating new one');
70-
this.randomizedUserId = randomBytes(USER_ID_BYTE_SIZE).toString('hex');
71-
}
77+
await this.init();
7278

7379
console.debug(`[ANALYTICS] getId() called (current ID: ${this.randomizedUserId})`);
7480
return this.randomizedUserId;
@@ -104,12 +110,14 @@ export class UserIdService implements UserIdServiceInterface {
104110
this.walletBasedUserId = undefined;
105111
this.userTrackingType$.next(UserTrackingType.Basic);
106112
this.clearSessionTimeout();
113+
this.hasNewSessionStarted = false;
107114
await this.clearStorage({ keys: ['userId', 'usePersistentUserId'] });
108115
}
109116

110117
async makePersistent(): Promise<void> {
111118
console.debug('[ANALYTICS] Converting user ID into persistent');
112119
this.clearSessionTimeout();
120+
this.setSessionTimeout();
113121
const userId = await this.getRandomizedUserId();
114122
await this.setStorage({ usePersistentUserId: true, userId });
115123
this.userTrackingType$.next(UserTrackingType.Enhanced);
@@ -123,9 +131,6 @@ export class UserIdService implements UserIdServiceInterface {
123131
}
124132

125133
async extendLifespan(): Promise<void> {
126-
if (!this.sessionTimeout) {
127-
return;
128-
}
129134
console.debug('[ANALYTICS] Extending temporary ID lifespan');
130135
this.clearSessionTimeout();
131136
this.setSessionTimeout();
@@ -151,8 +156,10 @@ export class UserIdService implements UserIdServiceInterface {
151156
return;
152157
}
153158
this.sessionTimeout = setTimeout(() => {
154-
this.randomizedUserId = undefined;
155-
console.debug('[ANALYTICS] Session timed out');
159+
if (this.userTrackingType$.value === UserTrackingType.Basic) {
160+
this.randomizedUserId = undefined;
161+
}
162+
this.hasNewSessionStarted = false;
156163
}, this.sessionLength);
157164
}
158165

@@ -167,6 +174,12 @@ export class UserIdService implements UserIdServiceInterface {
167174
const hash = hashExtendedAccountPublicKey(extendedAccountPublicKey);
168175
return hashExtendedAccountPublicKey(hash);
169176
}
177+
178+
async isNewSession(): Promise<boolean> {
179+
const isNewSession = !this.hasNewSessionStarted;
180+
this.hasNewSessionStarted = true;
181+
return isNewSession;
182+
}
170183
}
171184

172185
const userIdService = new UserIdService();

apps/browser-extension-wallet/src/lib/scripts/types/userIdService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export interface UserIdService {
1313
extendLifespan(): Promise<void>;
1414
resetToDefaultValues(): Promise<void>;
1515
userTrackingType$: BehaviorSubject<UserTrackingType>;
16+
isNewSession(): Promise<boolean>;
1617
}

apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/AnalyticsTracker.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,20 @@ describe('AnalyticsTracker', () => {
157157
});
158158
});
159159

160+
describe('posthog sendSessionStartEvent', () => {
161+
it('should send start session event', async () => {
162+
const tracker = new AnalyticsTracker({
163+
chain: preprodChain,
164+
postHogClient: getPostHogClient()
165+
});
166+
const mockedPostHogClient = (PostHogClient as any).mock.instances[0];
167+
const event = PostHogAction.OnboardingCreateClick;
168+
await tracker.sendEventToPostHog(event);
169+
expect(mockedPostHogClient.sendSessionStartEvent).toHaveBeenCalled();
170+
expect(mockedPostHogClient.sendEvent).toHaveBeenCalledWith(event, {});
171+
});
172+
});
173+
160174
describe('excluded events', () => {
161175
it('should ommit sending onboarding | new wallet events', async () => {
162176
const tracker = new AnalyticsTracker({

apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/AnalyticsTracker.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,31 +61,46 @@ export class AnalyticsTracker implements IAnalyticsTracker {
6161
}
6262
}
6363

64+
private async checkNewSessionStarted(): Promise<void> {
65+
if (!this.postHogClient) {
66+
console.debug('[ANALYTICS] no posthog client');
67+
return;
68+
}
69+
if (await this.userIdService.isNewSession()) {
70+
await this.postHogClient.sendSessionStartEvent();
71+
}
72+
}
73+
6474
async sendPageNavigationEvent(): Promise<void> {
6575
const shouldOmitEvent = this.shouldOmitSendEventToPostHog();
6676
if (shouldOmitEvent) return;
77+
await this.userIdService?.extendLifespan();
78+
await this.checkNewSessionStarted();
6779
await this.postHogClient?.sendPageNavigationEvent();
6880
}
6981

7082
async sendAliasEvent(): Promise<void> {
7183
const shouldOmitEvent = this.shouldOmitSendEventToPostHog();
7284
if (shouldOmitEvent) return;
85+
await this.userIdService?.extendLifespan();
86+
await this.checkNewSessionStarted();
7387
await this.postHogClient?.sendAliasEvent();
7488
}
7589

7690
async sendEventToMatomo(props: MatomoSendEventProps): Promise<void> {
7791
const isOptedOutUser = this.userTrackingType === UserTrackingType.Basic;
7892
if (MATOMO_OPTED_OUT_EVENTS_DISABLED && isOptedOutUser) return;
79-
await this.matomoClient?.sendEvent(props);
8093
await this.userIdService?.extendLifespan();
94+
await this.matomoClient?.sendEvent(props);
8195
}
8296

8397
async sendEventToPostHog(action: PostHogAction, properties: PostHogProperties = {}): Promise<void> {
8498
const isEventExcluded = this.isEventExcluded(action);
8599
const shouldOmitEvent = this.shouldOmitSendEventToPostHog();
86100
if (shouldOmitEvent || isEventExcluded) return;
87-
await this.postHogClient?.sendEvent(action, properties);
88101
await this.userIdService?.extendLifespan();
102+
await this.checkNewSessionStarted();
103+
await this.postHogClient?.sendEvent(action, properties);
89104
}
90105

91106
setChain(chain: Wallet.Cardano.ChainId): void {

apps/browser-extension-wallet/src/providers/AnalyticsProvider/context.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ export const AnalyticsProvider = ({
6161
// Track page changes with PostHog in order to keep the user session alive
6262
useEffect(() => {
6363
const trackActivePageChange = debounce(() => analyticsTracker.sendPageNavigationEvent(), PAGE_VIEW_DEBOUNCE_DELAY);
64-
64+
window.addEventListener('load', trackActivePageChange);
6565
window.addEventListener('popstate', trackActivePageChange);
6666
return () => {
67+
window.removeEventListener('load', trackActivePageChange);
6768
window.removeEventListener('popstate', trackActivePageChange);
6869
};
6970
}, [analyticsTracker]);

apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/PostHogClient.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ describe('PostHogClient', () => {
5656
);
5757
});
5858

59+
it('should send session started event', async () => {
60+
const client = new PostHogClient(chain, mockUserIdService, mockBackgroundStorageUtil, undefined, publicPosthogHost);
61+
await client.sendSessionStartEvent();
62+
expect(posthog.capture).toHaveBeenCalledWith(
63+
PostHogAction.WalletSessionStartPageview,
64+
expect.objectContaining({
65+
// eslint-disable-next-line camelcase
66+
distinct_id: userId,
67+
view: 'extended'
68+
})
69+
);
70+
});
71+
5972
it('should send events with distinct id', async () => {
6073
const client = new PostHogClient(chain, mockUserIdService, mockBackgroundStorageUtil, undefined, publicPosthogHost);
6174
const event = PostHogAction.OnboardingCreateClick;

apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/PostHogClient.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ export class PostHogClient {
120120
});
121121
}
122122

123+
async sendSessionStartEvent(): Promise<void> {
124+
console.debug('[ANALYTICS] Logging Session Start Event');
125+
posthog.capture(String(PostHogAction.WalletSessionStartPageview), {
126+
...(await this.getEventMetadata())
127+
});
128+
}
129+
123130
async sendPageNavigationEvent(): Promise<void> {
124131
console.debug('[ANALYTICS] Logging page navigation event to PostHog');
125132

apps/browser-extension-wallet/src/utils/mocks/test-helpers.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,9 @@ export const userIdServiceMock: Record<keyof UserIdService, jest.Mock> = {
604604
getRandomizedUserId: jest.fn(),
605605
getUserId: jest.fn(),
606606
getAliasProperties: jest.fn(),
607-
resetToDefaultValues: jest.fn(),
608-
userTrackingType$: new Subject() as any
607+
userTrackingType$: new Subject() as any,
608+
isNewSession: jest.fn(() => true),
609+
resetToDefaultValues: jest.fn()
609610
};
610611

611612
export const matomoClientMocks: Record<keyof typeof MatomoClient.prototype, jest.Mock> = {
@@ -624,7 +625,8 @@ export const postHogClientMocks: Record<keyof typeof PostHogClient.prototype, je
624625
overrideFeatureFlags: jest.fn(),
625626
getExperimentVariant: jest.fn(),
626627
subscribeToDistinctIdUpdate: jest.fn(),
627-
shutdown: jest.fn()
628+
shutdown: jest.fn(),
629+
sendSessionStartEvent: jest.fn()
628630
};
629631

630632
export const mockAnalyticsTracker: Record<keyof typeof AnalyticsTracker.prototype, jest.Mock> = {

0 commit comments

Comments
 (0)