Skip to content

Commit d5c6303

Browse files
authored
Merge pull request #20164 from mozilla/PAY-3544
feat(payments-events): record free trial metrics
2 parents 5d1e102 + 1ba75ff commit d5c6303

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
@@ -15,6 +15,7 @@ import {
1515
PageName,
1616
Step,
1717
SubscriptionCancellationData,
18+
TrialConversionData,
1819
type AccountsMetricsData,
1920
type ExperimentationData,
2021
type GenericGleanSubManageEvent,
@@ -51,6 +52,7 @@ export const CommonMetricsFactory = (
5152
ipAddress: faker.internet.ip(),
5253
deviceType: faker.string.alphanumeric(),
5354
userAgent: faker.internet.userAgent(),
55+
isFreeTrial: false,
5456
params: {},
5557
searchParams: {},
5658
experimentationId: faker.string.uuid(),
@@ -101,6 +103,19 @@ export const SubscriptionCancellationDataFactory = (
101103
};
102104
};
103105

106+
export const TrialConversionDataFactory = (
107+
override?: Partial<TrialConversionData>
108+
): TrialConversionData => ({
109+
conversionStatus: faker.helpers.arrayElement([
110+
'successful',
111+
'unsuccessful',
112+
]),
113+
providerEventId: `evt_${faker.string.alphanumeric({ length: 24 })}`,
114+
productId: `prod_${faker.string.alphanumeric({ length: 14 })}`,
115+
billingCountry: faker.location.countryCode(),
116+
...override,
117+
});
118+
104119
export const ExperimentationDataFactory = (
105120
override?: Partial<ExperimentationData>
106121
): 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
@@ -26,6 +26,7 @@ export type CommonMetrics = {
2626
deviceType: string;
2727
userAgent: string;
2828
experimentationId: string;
29+
isFreeTrial: boolean;
2930
params: Record<string, string>;
3031
searchParams: Record<string, string>;
3132
};
@@ -107,6 +108,13 @@ export type SubscriptionCancellationData = {
107108
providerEventId: string;
108109
};
109110

111+
export type TrialConversionData = {
112+
conversionStatus: 'successful' | 'unsuccessful';
113+
providerEventId: string;
114+
productId: string;
115+
billingCountry?: string;
116+
};
117+
110118
export const PaymentsGleanProvider = Symbol('GleanServerEventsProvider');
111119

112120
export type PaymentsGleanServerEventsLoggerTester = {
@@ -116,6 +124,7 @@ export type PaymentsGleanServerEventsLoggerTester = {
116124
recordPaySetupSuccess: (data: any) => void;
117125
recordPaySetupFail: (data: any) => void;
118126
recordSubscriptionEnded: (data: any) => void;
127+
recordSubscriptionTrialConverted: (data: any) => void;
119128
};
120129

121130
export type PageName =

0 commit comments

Comments
 (0)