Skip to content

Commit 1ba75ff

Browse files
committed
feat(payments-events): record free trial metrics
1 parent 0b72bca commit 1ba75ff

31 files changed

Lines changed: 663 additions & 26 deletions

libs/payments/events/src/lib/emitter.factories.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
AdditionalMetricsData,
77
SP3RolloutEvent,
88
SubscriptionEndedEvents,
9+
TrialConvertedEvents,
910
type AuthEvents,
1011
} from './emitter.types';
1112
import {
@@ -49,6 +50,18 @@ export const SubscriptionEndedFactory = (
4950
...override,
5051
});
5152

53+
export const TrialConvertedFactory = (
54+
override?: Partial<TrialConvertedEvents>
55+
): TrialConvertedEvents => ({
56+
productId: `prod_${faker.string.alphanumeric({ length: 24 })}`,
57+
priceId: `price_${faker.string.alphanumeric({ length: 24 })}`,
58+
conversionStatus: faker.helpers.arrayElement(['successful', 'unsuccessful']),
59+
providerEventId: `evt_${faker.string.alphanumeric({ length: 24 })}`,
60+
uid: faker.string.uuid(),
61+
billingCountry: faker.location.countryCode(),
62+
...override,
63+
});
64+
5265
export const SP3RolloutEventFactory = (
5366
override?: Partial<SP3RolloutEvent>
5467
): SP3RolloutEvent => ({

libs/payments/events/src/lib/emitter.service.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
AuthEventsFactory,
5151
SP3RolloutEventFactory,
5252
SubscriptionEndedFactory,
53+
TrialConvertedFactory,
5354
} from './emitter.factories';
5455
import {
5556
AccountFactory,
@@ -685,6 +686,59 @@ describe('PaymentsEmitterService', () => {
685686
});
686687
});
687688

689+
describe('handleTrialConverted', () => {
690+
const trialConvertedEventData = TrialConvertedFactory();
691+
692+
beforeEach(() => {
693+
jest
694+
.spyOn(paymentsGleanManager, 'recordFxaSubscriptionTrialConverted')
695+
.mockReturnValue();
696+
});
697+
698+
it('should call manager record method', async () => {
699+
await paymentsEmitterService.handleTrialConverted(
700+
trialConvertedEventData
701+
);
702+
expect(
703+
paymentsGleanManager.recordFxaSubscriptionTrialConverted
704+
).toHaveBeenCalledWith({
705+
cmsMetricsData: {
706+
priceId: trialConvertedEventData.priceId,
707+
productId: trialConvertedEventData.productId,
708+
},
709+
trialConversionData: {
710+
conversionStatus: trialConvertedEventData.conversionStatus,
711+
providerEventId: trialConvertedEventData.providerEventId,
712+
productId: trialConvertedEventData.productId,
713+
billingCountry: trialConvertedEventData.billingCountry,
714+
},
715+
});
716+
});
717+
718+
it('should not call manager record method if user has opted out', async () => {
719+
retrieveOptOutMock.mockRestore();
720+
721+
const mockUid = 'f440f251e8af9b0cf4bb3037529eda40';
722+
const mockOptOutAccount = AccountFactory({
723+
metricsOptOutAt: 1,
724+
uid: Buffer.from(mockUid, 'hex'),
725+
});
726+
jest
727+
.spyOn(accountManager, 'getAccounts')
728+
.mockResolvedValue([mockOptOutAccount]);
729+
730+
const eventData = {
731+
...trialConvertedEventData,
732+
uid: mockUid,
733+
};
734+
await paymentsEmitterService.handleTrialConverted(eventData);
735+
expect(accountManager.getAccounts).toHaveBeenCalledWith([mockUid]);
736+
expect(
737+
paymentsGleanManager.recordFxaSubscriptionTrialConverted
738+
).not.toHaveBeenCalled();
739+
});
740+
});
741+
688742
describe('handleSP3Rollout', () => {
689743
const completeEventData = SP3RolloutEventFactory();
690744
beforeEach(() => {

libs/payments/events/src/lib/emitter.service.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
PaymentsEmitterEvents,
1818
SP3RolloutEvent,
1919
SubscriptionEndedEvents,
20+
TrialConvertedEvents,
2021
type AuthEvents,
2122
} from './emitter.types';
2223
import { AccountManager } from '@fxa/shared/account/account';
@@ -60,6 +61,10 @@ export class PaymentsEmitterService {
6061
'subscriptionEnded',
6162
this.handleSubscriptionEnded.bind(this)
6263
);
64+
this.emitter.on(
65+
'trialConverted',
66+
this.handleTrialConverted.bind(this)
67+
);
6368
this.emitter.on('sp3Rollout', this.handleSP3Rollout.bind(this));
6469
this.emitter.on('locationView', this.handleLocationView.bind(this));
6570
this.emitter.on('auth', this.handleAuthEvent.bind(this));
@@ -337,6 +342,34 @@ export class PaymentsEmitterService {
337342
}
338343
}
339344

345+
async handleTrialConverted(eventData: TrialConvertedEvents) {
346+
const {
347+
productId,
348+
priceId,
349+
conversionStatus,
350+
providerEventId,
351+
uid,
352+
billingCountry,
353+
} = eventData;
354+
355+
const metricsOptOut = await this.retrieveOptOut(uid);
356+
357+
if (!metricsOptOut) {
358+
this.paymentsGleanManager.recordFxaSubscriptionTrialConverted({
359+
cmsMetricsData: {
360+
priceId,
361+
productId,
362+
},
363+
trialConversionData: {
364+
conversionStatus,
365+
providerEventId,
366+
productId,
367+
billingCountry,
368+
},
369+
});
370+
}
371+
}
372+
340373
async handleSP3Rollout(eventData: SP3RolloutEvent) {
341374
const { version, offeringId, interval, shadowMode } = eventData;
342375

libs/payments/events/src/lib/emitter.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ export type SubscriptionEndedEvents = {
4242
uid?: string;
4343
};
4444

45+
export type TrialConvertedEvents = {
46+
productId: string;
47+
priceId: string;
48+
conversionStatus: 'successful' | 'unsuccessful';
49+
providerEventId: string;
50+
uid?: string;
51+
billingCountry?: string;
52+
};
53+
4554
export const PaymentsEmitterEventsKeys = [
4655
'checkoutView',
4756
'checkoutEngage',
@@ -78,6 +87,7 @@ export type PaymentsEmitterEvents = {
7887
genericGleanEvent: GenericGleanEvent;
7988
genericGleanSubManageEvent: GenericGleanSubManageEvent;
8089
subscriptionEnded: SubscriptionEndedEvents;
90+
trialConverted: TrialConvertedEvents;
8191
sp3Rollout: SP3RolloutEvent;
8292
locationView: LocationStatus | TaxChangeAllowedStatus;
8393
auth: AuthEvents;

libs/payments/metrics/src/lib/glean/glean.factory.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CmsMetricsData,
99
CommonMetrics,
1010
SubscriptionCancellationData,
11+
TrialConversionData,
1112
type AccountsMetricsData,
1213
type ExperimentationData,
1314
type GenericGleanSubManageEvent,
@@ -41,6 +42,7 @@ export const CommonMetricsFactory = (
4142
ipAddress: faker.internet.ip(),
4243
deviceType: faker.string.alphanumeric(),
4344
userAgent: faker.internet.userAgent(),
45+
isFreeTrial: false,
4446
params: {},
4547
searchParams: {},
4648
experimentationId: faker.string.uuid(),
@@ -91,6 +93,19 @@ export const SubscriptionCancellationDataFactory = (
9193
};
9294
};
9395

96+
export const TrialConversionDataFactory = (
97+
override?: Partial<TrialConversionData>
98+
): TrialConversionData => ({
99+
conversionStatus: faker.helpers.arrayElement([
100+
'successful',
101+
'unsuccessful',
102+
]),
103+
providerEventId: `evt_${faker.string.alphanumeric({ length: 24 })}`,
104+
productId: `prod_${faker.string.alphanumeric({ length: 14 })}`,
105+
billingCountry: faker.location.countryCode(),
106+
...override,
107+
});
108+
94109
export const ExperimentationDataFactory = (
95110
override?: Partial<ExperimentationData>
96111
): ExperimentationData => ({

libs/payments/metrics/src/lib/glean/glean.manager.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CommonMetricsFactory,
1010
ExperimentationDataFactory,
1111
SubscriptionCancellationDataFactory,
12+
TrialConversionDataFactory,
1213
} from './glean.factory';
1314
import { PaymentsGleanProvider } from './glean.types';
1415
import { MockPaymentsGleanFactory } from './glean.test-provider';
@@ -209,6 +210,44 @@ describe('PaymentsGleanManager', () => {
209210
});
210211
});
211212

213+
describe('recordFxaSubscriptionTrialConverted', () => {
214+
beforeEach(() => {
215+
jest
216+
.spyOn(
217+
paymentsGleanServerEventsLogger,
218+
'recordSubscriptionTrialConverted'
219+
)
220+
.mockReturnValue({});
221+
spyPopulateCommonMetrics = jest
222+
.spyOn(paymentsGleanManager as any, 'populateCommonMetrics')
223+
.mockReturnValue(mockCommonMetrics);
224+
});
225+
226+
it('should record subscription trial converted', () => {
227+
const mockTrialConversionData = TrialConversionDataFactory();
228+
const metricsData = {
229+
cmsMetricsData: mockCommonMetricsData.cmsMetricsData,
230+
trialConversionData: mockTrialConversionData,
231+
};
232+
paymentsGleanManager.recordFxaSubscriptionTrialConverted(
233+
metricsData,
234+
mockPaymentProvider
235+
);
236+
expect(spyPopulateCommonMetrics).toHaveBeenCalledWith(metricsData);
237+
expect(
238+
paymentsGleanServerEventsLogger.recordSubscriptionTrialConverted
239+
).toHaveBeenCalledWith({
240+
...mockCommonMetrics,
241+
subscription_payment_provider: mockPaymentProvider,
242+
subscription_product_id: mockTrialConversionData.productId,
243+
subscription_provider_event_id:
244+
mockTrialConversionData.providerEventId,
245+
trial_conversion_status: mockTrialConversionData.conversionStatus,
246+
subscription_billing_country: mockTrialConversionData.billingCountry,
247+
});
248+
});
249+
});
250+
212251
describe('enabled is false', () => {
213252
{
214253
let paymentsGleanManager: PaymentsGleanManager;

libs/payments/metrics/src/lib/glean/glean.manager.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CommonMetrics,
1010
PaymentsGleanProvider,
1111
SubscriptionCancellationData,
12+
TrialConversionData,
1213
type AccountsMetricsData,
1314
type ExperimentationData,
1415
type SessionMetricsData,
@@ -154,6 +155,35 @@ export class PaymentsGleanManager {
154155
}
155156
}
156157

158+
recordFxaSubscriptionTrialConverted(
159+
metrics: {
160+
cmsMetricsData: CmsMetricsData;
161+
trialConversionData: TrialConversionData;
162+
},
163+
paymentProvider?: PaymentProvidersType
164+
) {
165+
const commonMetrics = this.populateCommonMetrics(metrics);
166+
167+
if (this.isEnabled) {
168+
this.paymentsGleanServerEventsLogger.recordSubscriptionTrialConverted({
169+
...commonMetrics,
170+
subscription_payment_provider:
171+
normalizeGleanFalsyValues(paymentProvider),
172+
subscription_product_id: normalizeGleanFalsyValues(
173+
metrics.trialConversionData.productId
174+
),
175+
subscription_provider_event_id: normalizeGleanFalsyValues(
176+
metrics.trialConversionData.providerEventId
177+
),
178+
trial_conversion_status:
179+
metrics.trialConversionData.conversionStatus,
180+
subscription_billing_country: normalizeGleanFalsyValues(
181+
metrics.trialConversionData.billingCountry
182+
),
183+
});
184+
}
185+
}
186+
157187
recordGenericEvent(
158188
eventName: string,
159189
metrics: {
@@ -221,6 +251,11 @@ export class PaymentsGleanManager {
221251
subscriptionCancellationData?.cancellationReason ?? '',
222252
subscription_provider_event_id:
223253
subscriptionCancellationData?.providerEventId ?? '',
254+
subscription_is_free_trial: commonMetricsData.isFreeTrial
255+
? 'true'
256+
: '',
257+
trial_conversion_status: '',
258+
subscription_billing_country: '',
224259
utm_campaign: searchParams['utm_campaign'] ?? '',
225260
utm_content: searchParams['utm_content'] ?? '',
226261
utm_medium: searchParams['utm_medium'] ?? '',
@@ -243,6 +278,7 @@ export class PaymentsGleanManager {
243278
deviceType: '',
244279
userAgent: '',
245280
experimentationId: '',
281+
isFreeTrial: false,
246282
params: {},
247283
searchParams: {},
248284
};
@@ -289,6 +325,7 @@ export class PaymentsGleanManager {
289325
cartMetricsData,
290326
cmsMetricsData,
291327
subscriptionCancellationData,
328+
isFreeTrial: commonMetricsData.isFreeTrial,
292329
}),
293330
...mapUtm(commonMetricsData.searchParams),
294331
nimbus_user_id: experimentationData.nimbusUserId,

libs/payments/metrics/src/lib/glean/glean.test-provider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export const MockPaymentsGleanFactory = {
1515
recordPaySetupSuccess: () => {},
1616
recordPaySetupFail: () => {},
1717
recordSubscriptionEnded: () => {},
18+
recordSubscriptionTrialConverted: () => {},
1819
}) as any,
1920
};

libs/payments/metrics/src/lib/glean/glean.types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type CommonMetrics = {
2424
deviceType: string;
2525
userAgent: string;
2626
experimentationId: string;
27+
isFreeTrial: boolean;
2728
params: Record<string, string>;
2829
searchParams: Record<string, string>;
2930
};
@@ -102,6 +103,13 @@ export type SubscriptionCancellationData = {
102103
providerEventId: string;
103104
};
104105

106+
export type TrialConversionData = {
107+
conversionStatus: 'successful' | 'unsuccessful';
108+
providerEventId: string;
109+
productId: string;
110+
billingCountry?: string;
111+
};
112+
105113
export const PaymentsGleanProvider = Symbol('GleanServerEventsProvider');
106114

107115
export type PaymentsGleanServerEventsLoggerTester = {
@@ -111,4 +119,5 @@ export type PaymentsGleanServerEventsLoggerTester = {
111119
recordPaySetupSuccess: (data: any) => void;
112120
recordPaySetupFail: (data: any) => void;
113121
recordSubscriptionEnded: (data: any) => void;
122+
recordSubscriptionTrialConverted: (data: any) => void;
114123
};

0 commit comments

Comments
 (0)