Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions libs/payments/events/src/lib/emitter.factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AdditionalMetricsData,
SP3RolloutEvent,
SubscriptionEndedEvents,
TrialConvertedEvents,
type AuthEvents,
} from './emitter.types';
import {
Expand Down Expand Up @@ -49,6 +50,18 @@ export const SubscriptionEndedFactory = (
...override,
});

export const TrialConvertedFactory = (
override?: Partial<TrialConvertedEvents>
): TrialConvertedEvents => ({
productId: `prod_${faker.string.alphanumeric({ length: 24 })}`,
priceId: `price_${faker.string.alphanumeric({ length: 24 })}`,
conversionStatus: faker.helpers.arrayElement(['successful', 'unsuccessful']),
providerEventId: `evt_${faker.string.alphanumeric({ length: 24 })}`,
uid: faker.string.uuid(),
billingCountry: faker.location.countryCode(),
...override,
});

export const SP3RolloutEventFactory = (
override?: Partial<SP3RolloutEvent>
): SP3RolloutEvent => ({
Expand Down
54 changes: 54 additions & 0 deletions libs/payments/events/src/lib/emitter.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
AuthEventsFactory,
SP3RolloutEventFactory,
SubscriptionEndedFactory,
TrialConvertedFactory,
} from './emitter.factories';
import {
AccountFactory,
Expand Down Expand Up @@ -685,6 +686,59 @@ describe('PaymentsEmitterService', () => {
});
});

describe('handleTrialConverted', () => {
const trialConvertedEventData = TrialConvertedFactory();

beforeEach(() => {
jest
.spyOn(paymentsGleanManager, 'recordFxaSubscriptionTrialConverted')
.mockReturnValue();
});

it('should call manager record method', async () => {
await paymentsEmitterService.handleTrialConverted(
trialConvertedEventData
);
expect(
paymentsGleanManager.recordFxaSubscriptionTrialConverted
).toHaveBeenCalledWith({
cmsMetricsData: {
priceId: trialConvertedEventData.priceId,
productId: trialConvertedEventData.productId,
},
trialConversionData: {
conversionStatus: trialConvertedEventData.conversionStatus,
providerEventId: trialConvertedEventData.providerEventId,
productId: trialConvertedEventData.productId,
billingCountry: trialConvertedEventData.billingCountry,
},
});
});

it('should not call manager record method if user has opted out', async () => {
retrieveOptOutMock.mockRestore();

const mockUid = 'f440f251e8af9b0cf4bb3037529eda40';
const mockOptOutAccount = AccountFactory({
metricsOptOutAt: 1,
uid: Buffer.from(mockUid, 'hex'),
});
jest
.spyOn(accountManager, 'getAccounts')
.mockResolvedValue([mockOptOutAccount]);

const eventData = {
...trialConvertedEventData,
uid: mockUid,
};
await paymentsEmitterService.handleTrialConverted(eventData);
expect(accountManager.getAccounts).toHaveBeenCalledWith([mockUid]);
expect(
paymentsGleanManager.recordFxaSubscriptionTrialConverted
).not.toHaveBeenCalled();
});
});

describe('handleSP3Rollout', () => {
const completeEventData = SP3RolloutEventFactory();
beforeEach(() => {
Expand Down
33 changes: 33 additions & 0 deletions libs/payments/events/src/lib/emitter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PaymentsEmitterEvents,
SP3RolloutEvent,
SubscriptionEndedEvents,
TrialConvertedEvents,
type AuthEvents,
} from './emitter.types';
import { AccountManager } from '@fxa/shared/account/account';
Expand Down Expand Up @@ -60,6 +61,10 @@ export class PaymentsEmitterService {
'subscriptionEnded',
this.handleSubscriptionEnded.bind(this)
);
this.emitter.on(
'trialConverted',
this.handleTrialConverted.bind(this)
);
this.emitter.on('sp3Rollout', this.handleSP3Rollout.bind(this));
this.emitter.on('locationView', this.handleLocationView.bind(this));
this.emitter.on('auth', this.handleAuthEvent.bind(this));
Expand Down Expand Up @@ -337,6 +342,34 @@ export class PaymentsEmitterService {
}
}

async handleTrialConverted(eventData: TrialConvertedEvents) {
const {
productId,
priceId,
conversionStatus,
providerEventId,
uid,
billingCountry,
} = eventData;

const metricsOptOut = await this.retrieveOptOut(uid);

if (!metricsOptOut) {
this.paymentsGleanManager.recordFxaSubscriptionTrialConverted({
cmsMetricsData: {
priceId,
productId,
},
trialConversionData: {
conversionStatus,
providerEventId,
productId,
billingCountry,
},
});
}
}

async handleSP3Rollout(eventData: SP3RolloutEvent) {
const { version, offeringId, interval, shadowMode } = eventData;

Expand Down
10 changes: 10 additions & 0 deletions libs/payments/events/src/lib/emitter.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ export type SubscriptionEndedEvents = {
uid?: string;
};

export type TrialConvertedEvents = {
productId: string;
priceId: string;
conversionStatus: 'successful' | 'unsuccessful';
providerEventId: string;
uid?: string;
billingCountry?: string;
};

export const PaymentsEmitterEventsKeys = [
'checkoutView',
'checkoutEngage',
Expand Down Expand Up @@ -78,6 +87,7 @@ export type PaymentsEmitterEvents = {
genericGleanEvent: GenericGleanEvent;
genericGleanSubManageEvent: GenericGleanSubManageEvent;
subscriptionEnded: SubscriptionEndedEvents;
trialConverted: TrialConvertedEvents;
sp3Rollout: SP3RolloutEvent;
locationView: LocationStatus | TaxChangeAllowedStatus;
auth: AuthEvents;
Expand Down
15 changes: 15 additions & 0 deletions libs/payments/metrics/src/lib/glean/glean.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CmsMetricsData,
CommonMetrics,
SubscriptionCancellationData,
TrialConversionData,
type AccountsMetricsData,
type ExperimentationData,
type GenericGleanSubManageEvent,
Expand Down Expand Up @@ -41,6 +42,7 @@ export const CommonMetricsFactory = (
ipAddress: faker.internet.ip(),
deviceType: faker.string.alphanumeric(),
userAgent: faker.internet.userAgent(),
isFreeTrial: false,
params: {},
searchParams: {},
experimentationId: faker.string.uuid(),
Expand Down Expand Up @@ -91,6 +93,19 @@ export const SubscriptionCancellationDataFactory = (
};
};

export const TrialConversionDataFactory = (
override?: Partial<TrialConversionData>
): TrialConversionData => ({
conversionStatus: faker.helpers.arrayElement([
'successful',
'unsuccessful',
]),
providerEventId: `evt_${faker.string.alphanumeric({ length: 24 })}`,
productId: `prod_${faker.string.alphanumeric({ length: 14 })}`,
billingCountry: faker.location.countryCode(),
...override,
});

export const ExperimentationDataFactory = (
override?: Partial<ExperimentationData>
): ExperimentationData => ({
Expand Down
39 changes: 39 additions & 0 deletions libs/payments/metrics/src/lib/glean/glean.manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CommonMetricsFactory,
ExperimentationDataFactory,
SubscriptionCancellationDataFactory,
TrialConversionDataFactory,
} from './glean.factory';
import { PaymentsGleanProvider } from './glean.types';
import { MockPaymentsGleanFactory } from './glean.test-provider';
Expand Down Expand Up @@ -209,6 +210,44 @@ describe('PaymentsGleanManager', () => {
});
});

describe('recordFxaSubscriptionTrialConverted', () => {
beforeEach(() => {
jest
.spyOn(
paymentsGleanServerEventsLogger,
'recordSubscriptionTrialConverted'
)
.mockReturnValue({});
spyPopulateCommonMetrics = jest
.spyOn(paymentsGleanManager as any, 'populateCommonMetrics')
.mockReturnValue(mockCommonMetrics);
});

it('should record subscription trial converted', () => {
const mockTrialConversionData = TrialConversionDataFactory();
const metricsData = {
cmsMetricsData: mockCommonMetricsData.cmsMetricsData,
trialConversionData: mockTrialConversionData,
};
paymentsGleanManager.recordFxaSubscriptionTrialConverted(
metricsData,
mockPaymentProvider
);
expect(spyPopulateCommonMetrics).toHaveBeenCalledWith(metricsData);
expect(
paymentsGleanServerEventsLogger.recordSubscriptionTrialConverted
).toHaveBeenCalledWith({
...mockCommonMetrics,
subscription_payment_provider: mockPaymentProvider,
subscription_product_id: mockTrialConversionData.productId,
subscription_provider_event_id:
mockTrialConversionData.providerEventId,
trial_conversion_status: mockTrialConversionData.conversionStatus,
subscription_billing_country: mockTrialConversionData.billingCountry,
});
});
});

describe('enabled is false', () => {
{
let paymentsGleanManager: PaymentsGleanManager;
Expand Down
37 changes: 37 additions & 0 deletions libs/payments/metrics/src/lib/glean/glean.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CommonMetrics,
PaymentsGleanProvider,
SubscriptionCancellationData,
TrialConversionData,
type AccountsMetricsData,
type ExperimentationData,
type SessionMetricsData,
Expand Down Expand Up @@ -154,6 +155,35 @@ export class PaymentsGleanManager {
}
}

recordFxaSubscriptionTrialConverted(
metrics: {
cmsMetricsData: CmsMetricsData;
trialConversionData: TrialConversionData;
},
paymentProvider?: PaymentProvidersType
) {
const commonMetrics = this.populateCommonMetrics(metrics);

if (this.isEnabled) {
this.paymentsGleanServerEventsLogger.recordSubscriptionTrialConverted({
...commonMetrics,
subscription_payment_provider:
normalizeGleanFalsyValues(paymentProvider),
subscription_product_id: normalizeGleanFalsyValues(
metrics.trialConversionData.productId
),
subscription_provider_event_id: normalizeGleanFalsyValues(
metrics.trialConversionData.providerEventId
),
trial_conversion_status:
metrics.trialConversionData.conversionStatus,
subscription_billing_country: normalizeGleanFalsyValues(
metrics.trialConversionData.billingCountry
),
});
}
}

recordGenericEvent(
eventName: string,
metrics: {
Expand Down Expand Up @@ -221,6 +251,11 @@ export class PaymentsGleanManager {
subscriptionCancellationData?.cancellationReason ?? '',
subscription_provider_event_id:
subscriptionCancellationData?.providerEventId ?? '',
subscription_is_free_trial: commonMetricsData.isFreeTrial
? 'true'
: '',
trial_conversion_status: '',
subscription_billing_country: '',
utm_campaign: searchParams['utm_campaign'] ?? '',
utm_content: searchParams['utm_content'] ?? '',
utm_medium: searchParams['utm_medium'] ?? '',
Expand All @@ -243,6 +278,7 @@ export class PaymentsGleanManager {
deviceType: '',
userAgent: '',
experimentationId: '',
isFreeTrial: false,
params: {},
searchParams: {},
};
Expand Down Expand Up @@ -289,6 +325,7 @@ export class PaymentsGleanManager {
cartMetricsData,
cmsMetricsData,
subscriptionCancellationData,
isFreeTrial: commonMetricsData.isFreeTrial,
}),
...mapUtm(commonMetricsData.searchParams),
nimbus_user_id: experimentationData.nimbusUserId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export const MockPaymentsGleanFactory = {
recordPaySetupSuccess: () => {},
recordPaySetupFail: () => {},
recordSubscriptionEnded: () => {},
recordSubscriptionTrialConverted: () => {},
}) as any,
};
9 changes: 9 additions & 0 deletions libs/payments/metrics/src/lib/glean/glean.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type CommonMetrics = {
deviceType: string;
userAgent: string;
experimentationId: string;
isFreeTrial: boolean;
params: Record<string, string>;
searchParams: Record<string, string>;
};
Expand Down Expand Up @@ -102,6 +103,13 @@ export type SubscriptionCancellationData = {
providerEventId: string;
};

export type TrialConversionData = {
conversionStatus: 'successful' | 'unsuccessful';
providerEventId: string;
productId: string;
billingCountry?: string;
};

export const PaymentsGleanProvider = Symbol('GleanServerEventsProvider');

export type PaymentsGleanServerEventsLoggerTester = {
Expand All @@ -111,4 +119,5 @@ export type PaymentsGleanServerEventsLoggerTester = {
recordPaySetupSuccess: (data: any) => void;
recordPaySetupFail: (data: any) => void;
recordSubscriptionEnded: (data: any) => void;
recordSubscriptionTrialConverted: (data: any) => void;
};
Loading
Loading