From 7dd569d383acd244c4437b4843e56d18bc0ceae1 Mon Sep 17 00:00:00 2001 From: Reino Muhl <10620585+StaberindeZA@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:26:29 -0400 Subject: [PATCH] feat(next): glean emit view event Because: - Record SubPlat P1 metric view event using glean This commit: - Adds payments-metrics library - Adds PaymentsGleanManager to format events data and CMS data into the required format for Glean events recording. - Adds PaymentsGleanService to handle metrics events emitted by Next.js - Adds the manager and service to the NestApp - Adds new config values to payments-next for Glean reporting - Emit view event from Checkout start page Closes #FXA-10087 --- .gitignore | 4 +- apps/payments/next/.env.development | 6 + apps/payments/next/.env.production | 6 + .../[interval]/[cartId]/start/page.tsx | 20 +++- apps/payments/next/config/index.ts | 8 +- .../next/lib/utils/getCommonGleanMetrics.ts | 16 +++ apps/payments/next/lib/utils/getIpAddress.ts | 9 ++ apps/payments/next/next.config.js | 2 + apps/payments/next/project.json | 2 +- libs/payments/metrics/.eslintrc.json | 18 +++ libs/payments/metrics/.swcrc | 15 +++ libs/payments/metrics/README.md | 15 +++ libs/payments/metrics/jest.config.ts | 30 +++++ libs/payments/metrics/package.json | 8 ++ libs/payments/metrics/project.json | 45 +++++++ libs/payments/metrics/src/index.ts | 8 ++ .../src/lib/glean/__generated__}/.gitkeep | 0 .../metrics/src/lib/glean/glean.config.ts | 24 ++++ .../metrics/src/lib/glean/glean.factory.ts | 85 ++++++++++++++ .../src/lib/glean/glean.manager.spec.ts | 57 +++++++++ .../metrics/src/lib/glean/glean.manager.ts | 58 +++++++++ .../metrics/src/lib/glean/glean.provider.ts | 39 +++++++ .../src/lib/glean/glean.service.spec.ts | 110 ++++++++++++++++++ .../metrics/src/lib/glean/glean.service.ts | 56 +++++++++ .../metrics/src/lib/glean/glean.types.ts | 36 ++++++ .../src/lib/glean/utils/mapParams.spec.ts | 22 ++++ .../metrics/src/lib/glean/utils/mapParams.ts | 17 +++ .../lib/glean/utils/mapRelyingParty.spec.ts | 20 ++++ .../src/lib/glean/utils/mapRelyingParty.ts | 11 ++ .../src/lib/glean/utils/mapSession.spec.ts | 31 +++++ .../metrics/src/lib/glean/utils/mapSession.ts | 19 +++ .../lib/glean/utils/mapSubscription.spec.ts | 66 +++++++++++ .../src/lib/glean/utils/mapSubscription.ts | 37 ++++++ .../src/lib/glean/utils/mapUtm.spec.ts | 27 +++++ .../metrics/src/lib/glean/utils/mapUtm.ts | 14 +++ .../utils/normalizeGleanFalsyValues.spec.ts | 14 +++ .../glean/utils/normalizeGleanFalsyValues.ts | 6 + libs/payments/metrics/tsconfig.json | 22 ++++ libs/payments/metrics/tsconfig.lib.json | 10 ++ libs/payments/metrics/tsconfig.spec.json | 14 +++ libs/payments/paypal/src/lib/paypal.client.ts | 2 +- libs/payments/paypal/src/lib/util.ts | 4 +- .../payments/ui/src/lib/nestapp/app.module.ts | 19 ++- libs/payments/ui/src/lib/nestapp/app.ts | 5 + libs/payments/ui/src/lib/nestapp/config.ts | 6 + .../firestore/src/lib/firestore.provider.ts | 2 +- .../lib/type-cachable-firestore-adapter.ts | 2 +- .../src/registry/subplat-backend-metrics.yaml | 58 ++++++--- package.json | 1 + tsconfig.base.json | 5 +- yarn.lock | 1 + 51 files changed, 1086 insertions(+), 26 deletions(-) create mode 100644 apps/payments/next/lib/utils/getCommonGleanMetrics.ts create mode 100644 apps/payments/next/lib/utils/getIpAddress.ts create mode 100644 libs/payments/metrics/.eslintrc.json create mode 100644 libs/payments/metrics/.swcrc create mode 100644 libs/payments/metrics/README.md create mode 100644 libs/payments/metrics/jest.config.ts create mode 100644 libs/payments/metrics/package.json create mode 100644 libs/payments/metrics/project.json create mode 100644 libs/payments/metrics/src/index.ts rename {apps/payments/next/lib/metrics/glean => libs/payments/metrics/src/lib/glean/__generated__}/.gitkeep (100%) create mode 100644 libs/payments/metrics/src/lib/glean/glean.config.ts create mode 100644 libs/payments/metrics/src/lib/glean/glean.factory.ts create mode 100644 libs/payments/metrics/src/lib/glean/glean.manager.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/glean.manager.ts create mode 100644 libs/payments/metrics/src/lib/glean/glean.provider.ts create mode 100644 libs/payments/metrics/src/lib/glean/glean.service.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/glean.service.ts create mode 100644 libs/payments/metrics/src/lib/glean/glean.types.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapParams.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapParams.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapSession.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapSession.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapSubscription.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapSubscription.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapUtm.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/mapUtm.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.spec.ts create mode 100644 libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.ts create mode 100644 libs/payments/metrics/tsconfig.json create mode 100644 libs/payments/metrics/tsconfig.lib.json create mode 100644 libs/payments/metrics/tsconfig.spec.json diff --git a/.gitignore b/.gitignore index be82451fb3e..8bd2b6dc787 100644 --- a/.gitignore +++ b/.gitignore @@ -157,7 +157,9 @@ packages/fxa-settings/test/ # payments-next apps/payments/next/public/locales -apps/payments/next/lib/metrics/glean/server_events.ts + +# payments-metrics +libs/payments/metrics/src/lib/glean/__generated__/server_events.ts Library .node diff --git a/apps/payments/next/.env.development b/apps/payments/next/.env.development index 9ccdda37557..7cc4b2f4f38 100644 --- a/apps/payments/next/.env.development +++ b/apps/payments/next/.env.development @@ -75,6 +75,12 @@ STATS_D_CONFIG__HOST= STATS_D_CONFIG__PORT= STATS_D_CONFIG__PREFIX= +# Glean Config +GLEAN_CONFIG__APPLICATION_ID= +# GLEAN_CONFIG__VERSION= # Set in next.config.js +GLEAN_CONFIG__CHANNEL='development' +GLEAN_CONFIG__LOGGER_APP_NAME='fxa-payments-next' + # CSP Config CSP__ACCOUNTS_STATIC_CDN=https://accounts-static.cdn.mozilla.net CSP__PAYPAL_API='https://www.sandbox.paypal.com' diff --git a/apps/payments/next/.env.production b/apps/payments/next/.env.production index 76849e816a8..b7e1b43f740 100644 --- a/apps/payments/next/.env.production +++ b/apps/payments/next/.env.production @@ -71,6 +71,12 @@ STATS_D_CONFIG__HOST= STATS_D_CONFIG__PORT= STATS_D_CONFIG__PREFIX= +# Glean Config +GLEAN_CONFIG__APPLICATION_ID= +# GLEAN_CONFIG__VERSION= # Set in next.config.js +GLEAN_CONFIG__CHANNEL='production' +GLEAN_CONFIG__LOGGER_APP_NAME='fxa-payments-next' + # CSP Config CSP__ACCOUNTS_STATIC_CDN=https://accounts-static.cdn.mozilla.net CSP__PAYPAL_API='https://www.paypal.com' diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx index c105c53d1b7..927f059cc9a 100644 --- a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx @@ -17,10 +17,17 @@ import { } from 'apps/payments/next/app/_lib/apiClient'; import { auth, signIn } from 'apps/payments/next/auth'; import { CheckoutParams } from '../layout'; +import { getCommonGleanMetrics } from 'apps/payments/next/lib/utils/getCommonGleanMetrics'; export const dynamic = 'force-dynamic'; -export default async function Checkout({ params }: { params: CheckoutParams }) { +export default async function Checkout({ + params, + searchParams, +}: { + params: CheckoutParams; + searchParams: Record; +}) { // Temporarily defaulting to `accept-language` // This to be updated in FXA-9404 //const locale = getLocaleFromRequest( @@ -44,6 +51,17 @@ export default async function Checkout({ params }: { params: CheckoutParams }) { cmsPromise, ]); + getApp() + .getGleanEmitter() + .emit('fxaPaySetupView', { + currency: 'USD', + checkoutType: 'without-accounts', + params: { ...params }, + searchParams, + ...cart, + ...getCommonGleanMetrics(), + }); + return (
{!session && ( diff --git a/apps/payments/next/config/index.ts b/apps/payments/next/config/index.ts index d04b9949df9..af2bcde8d47 100644 --- a/apps/payments/next/config/index.ts +++ b/apps/payments/next/config/index.ts @@ -104,4 +104,10 @@ export class PaymentsNextConfig extends NestAppRootConfig { nextPublicSentryTracesSampleRate!: number; } -export const config = validate(process.env, PaymentsNextConfig); +export const config = validate( + { + ...process.env, + GLEAN_CONFIG__VERSION: process.env['GLEAN_CONFIG__VERSION'], + }, + PaymentsNextConfig +); diff --git a/apps/payments/next/lib/utils/getCommonGleanMetrics.ts b/apps/payments/next/lib/utils/getCommonGleanMetrics.ts new file mode 100644 index 00000000000..3b3bd6635ba --- /dev/null +++ b/apps/payments/next/lib/utils/getCommonGleanMetrics.ts @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { headers } from 'next/headers'; +import { userAgentFromString } from 'next/server'; +import { getIpAddress } from './getIpAddress'; + +export function getCommonGleanMetrics() { + const userAgentString = headers().get('user-agent') || ''; + const userAgent = userAgentFromString(userAgentString); + return { + ipAddress: getIpAddress(), + userAgent: userAgentString, + deviceType: userAgent.device.type || 'desktop', + }; +} diff --git a/apps/payments/next/lib/utils/getIpAddress.ts b/apps/payments/next/lib/utils/getIpAddress.ts new file mode 100644 index 00000000000..bd2d2392bb3 --- /dev/null +++ b/apps/payments/next/lib/utils/getIpAddress.ts @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import 'server-only'; +import { headers } from 'next/headers'; + +export function getIpAddress() { + return (headers().get('x-forwarded-for') ?? '127.0.0.1').split(',')[0]; +} diff --git a/apps/payments/next/next.config.js b/apps/payments/next/next.config.js index bfdab67f129..6745b8bfaa2 100644 --- a/apps/payments/next/next.config.js +++ b/apps/payments/next/next.config.js @@ -20,6 +20,7 @@ const nextConfig = { }, env: { version, + GLEAN_CONFIG__VERSION: version, }, experimental: { instrumentationHook: true, @@ -34,6 +35,7 @@ const nextConfig = { 'hot-shots', 'knex', 'kysely', + 'mozlog', 'mysql2', 'nest-typed-config', 'superagent', diff --git a/apps/payments/next/project.json b/apps/payments/next/project.json index 8b83b527c31..306759f8be1 100644 --- a/apps/payments/next/project.json +++ b/apps/payments/next/project.json @@ -73,7 +73,7 @@ }, "glean-generate": { "dependsOn": ["glean-lint"], - "command": "yarn glean translate libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml -f typescript_server -o apps/payments/next/lib/metrics/glean" + "command": "yarn glean translate libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml -f typescript_server -o libs/payments/metrics/src/lib/glean/__generated__" }, "glean-lint": { "command": "yarn glean glinter libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml" diff --git a/libs/payments/metrics/.eslintrc.json b/libs/payments/metrics/.eslintrc.json new file mode 100644 index 00000000000..3456be9b903 --- /dev/null +++ b/libs/payments/metrics/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/payments/metrics/.swcrc b/libs/payments/metrics/.swcrc new file mode 100644 index 00000000000..42938ddcefd --- /dev/null +++ b/libs/payments/metrics/.swcrc @@ -0,0 +1,15 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + } + } +} + diff --git a/libs/payments/metrics/README.md b/libs/payments/metrics/README.md new file mode 100644 index 00000000000..eaf075dd794 --- /dev/null +++ b/libs/payments/metrics/README.md @@ -0,0 +1,15 @@ +# payments-metrics + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build payments-metrics` to build the library. + +## Running unit tests + +Run `nx run payments-metrics:test-unit` to execute the unit tests via [Jest](https://jestjs.io). + +## Running integration tests + +Run `nx run payments-metrics:test-integration` to execute the integration tests via [Jest](https://jestjs.io). diff --git a/libs/payments/metrics/jest.config.ts b/libs/payments/metrics/jest.config.ts new file mode 100644 index 00000000000..3b7a2cb92a1 --- /dev/null +++ b/libs/payments/metrics/jest.config.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +import { readFileSync } from 'fs'; + +// Reading the SWC compilation config and remove the "exclude" +// for the test files to be compiled by SWC +const { exclude: _, ...swcJestConfig } = JSON.parse( + readFileSync(`${__dirname}/.swcrc`, 'utf-8') +); + +// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. +// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" +if (swcJestConfig.swcrc === undefined) { + swcJestConfig.swcrc = false; +} + +// Uncomment if using global setup/teardown files being transformed via swc +// https://nx.dev/packages/jest/documents/overview#global-setup/teardown-with-nx-libraries +// jest needs EsModule Interop to find the default exported setup/teardown functions +// swcJestConfig.module.noInterop = false; + +export default { + displayName: 'payments-metrics', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/payments/metrics', +}; diff --git a/libs/payments/metrics/package.json b/libs/payments/metrics/package.json new file mode 100644 index 00000000000..749e02e1b97 --- /dev/null +++ b/libs/payments/metrics/package.json @@ -0,0 +1,8 @@ +{ + "name": "@fxa/payments/metrics", + "version": "0.0.1", + "dependencies": {}, + "type": "commonjs", + "main": "./index.cjs", + "private": true +} diff --git a/libs/payments/metrics/project.json b/libs/payments/metrics/project.json new file mode 100644 index 00000000000..356ca6e073f --- /dev/null +++ b/libs/payments/metrics/project.json @@ -0,0 +1,45 @@ +{ + "name": "payments-metrics", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/payments/metrics/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/payments/metrics", + "main": "libs/payments/metrics/src/index.ts", + "tsConfig": "libs/payments/metrics/tsconfig.lib.json", + "assets": ["libs/payments/metrics/*.md"], + "declaration": true + }, + "dependsOn": ["glean-generate"] + }, + "glean-generate": { + "dependsOn": ["glean-lint"], + "command": "yarn glean translate libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml -f typescript_server -o libs/payments/metrics/src/lib/glean/__generated__" + }, + "glean-lint": { + "command": "yarn glean glinter libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml" + }, + "test-unit": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/payments/metrics/jest.config.ts", + "testPathPattern": ["^(?!.*\\.in\\.spec\\.ts$).*$"] + }, + "dependsOn": ["glean-generate"] + }, + "test-integration": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/payments/metrics/jest.config.ts", + "testPathPattern": ["\\.in\\.spec\\.ts$"] + } + } + } +} diff --git a/libs/payments/metrics/src/index.ts b/libs/payments/metrics/src/index.ts new file mode 100644 index 00000000000..d003bc17192 --- /dev/null +++ b/libs/payments/metrics/src/index.ts @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +export * from './lib/glean/glean.types'; +export * from './lib/glean/glean.manager'; +export * from './lib/glean/glean.service'; +export * from './lib/glean/glean.config'; +export * from './lib/glean/glean.provider'; diff --git a/apps/payments/next/lib/metrics/glean/.gitkeep b/libs/payments/metrics/src/lib/glean/__generated__/.gitkeep similarity index 100% rename from apps/payments/next/lib/metrics/glean/.gitkeep rename to libs/payments/metrics/src/lib/glean/__generated__/.gitkeep diff --git a/libs/payments/metrics/src/lib/glean/glean.config.ts b/libs/payments/metrics/src/lib/glean/glean.config.ts new file mode 100644 index 00000000000..f3972b0de30 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.config.ts @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { IsEnum, IsString } from 'class-validator'; + +enum GleanChannel { + Development = 'development', + Stage = 'stage', + Production = 'production', +} + +export class PaymentsGleanConfig { + @IsString() + applicationId!: string; + + @IsString() + version!: string; + + @IsEnum(GleanChannel) + channel!: string; + + @IsString() + loggerAppName!: string; +} diff --git a/libs/payments/metrics/src/lib/glean/glean.factory.ts b/libs/payments/metrics/src/lib/glean/glean.factory.ts new file mode 100644 index 00000000000..a0824a9e1d2 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.factory.ts @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { faker } from '@faker-js/faker'; +import { + CartMetrics, + CmsMetricsData, + CommonMetrics, + FxaPaySetupMetrics, + FxaPaySetupViewMetrics, +} from './glean.types'; +import { ResultCartFactory } from '@fxa/payments/cart'; + +export const ParamsFactory = ( + override?: Record +): Record => ({ + locale: faker.helpers.arrayElement(['en-US', 'de', 'es', 'fr-FR']), + offeringId: faker.helpers.arrayElement([ + 'vpn', + 'relay-phone', + 'relay-email', + 'hubs', + 'mdnplus', + ]), + interval: faker.helpers.arrayElement([ + 'daily', + 'weekly', + 'monthly', + '6monthly', + 'yearly', + ]), + ...override, +}); + +export const CommonMetricsFactory = ( + override?: Partial +): CommonMetrics => ({ + ipAddress: faker.internet.ip(), + deviceType: faker.string.alphanumeric(), + userAgent: faker.internet.userAgent(), + params: {}, + searchParams: {}, + ...override, +}); + +export const CartMetricsFactory = ( + override?: Partial +): CartMetrics => { + const resultCart = ResultCartFactory(); + + return { + uid: resultCart.uid, + errorReasonId: resultCart.errorReasonId, + couponCode: resultCart.couponCode, + currency: faker.finance.currencyCode(), + ...override, + }; +}; + +export const FxaPaySetupMetricsFactory = ( + override?: Partial +): FxaPaySetupMetrics => ({ + ...CommonMetricsFactory(), + ...CartMetricsFactory(), + ...override, +}); + +export const FxaPaySetupViewMetricsFactory = ( + override?: Partial +): FxaPaySetupViewMetrics => ({ + ...FxaPaySetupMetricsFactory(), + checkoutType: faker.helpers.arrayElement([ + 'with-accounts', + 'without-accounts', + ]), + ...override, +}); + +export const CmsMetricsDataFactory = ( + override?: Partial +): CmsMetricsData => ({ + productId: `product_${faker.string.alphanumeric({ length: 14 })}`, + priceId: `price_${faker.string.alphanumeric({ length: 14 })}`, + ...override, +}); diff --git a/libs/payments/metrics/src/lib/glean/glean.manager.spec.ts b/libs/payments/metrics/src/lib/glean/glean.manager.spec.ts new file mode 100644 index 00000000000..2947455f5e9 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.manager.spec.ts @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { Test } from '@nestjs/testing'; +import { PaymentsGleanManager } from './glean.manager'; +import { + CmsMetricsDataFactory, + FxaPaySetupViewMetricsFactory, +} from './glean.factory'; +import { + MockPaymentsGleanFactory, + PaymentsGleanProvider, + PaymentsGleanServerEventsLogger, +} from './glean.provider'; + +describe('PaymentsGleanService', () => { + let paymentsGleanManager: PaymentsGleanManager; + let paymentsGleanServerEventsLogger: PaymentsGleanServerEventsLogger; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [MockPaymentsGleanFactory, PaymentsGleanManager], + }).compile(); + + paymentsGleanManager = moduleRef.get(PaymentsGleanManager); + paymentsGleanServerEventsLogger = moduleRef.get(PaymentsGleanProvider); + }); + + it('should be defined', () => { + expect(paymentsGleanManager).toBeDefined(); + }); + + describe('recordFxaPaySetupView', () => { + const mockCmsData = CmsMetricsDataFactory(); + const mockMetricsData = FxaPaySetupViewMetricsFactory(); + + beforeEach(() => { + jest + .spyOn(paymentsGleanServerEventsLogger, 'recordPaySetupView') + .mockReturnValue(); + }); + + it('should record fxa pay setup view', async () => { + await paymentsGleanManager.recordFxaPaySetupView( + mockMetricsData, + mockCmsData + ); + expect( + paymentsGleanServerEventsLogger.recordPaySetupView + ).toHaveBeenCalledWith( + expect.objectContaining({ + subscription_checkout_type: mockMetricsData.checkoutType, + }) + ); + }); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/glean.manager.ts b/libs/payments/metrics/src/lib/glean/glean.manager.ts new file mode 100644 index 00000000000..a12ca1d4224 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.manager.ts @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { CmsMetricsData, FxaPaySetupViewMetrics } from './glean.types'; +import { Inject, Injectable } from '@nestjs/common'; +import { + PaymentsGleanProvider, + type PaymentsGleanServerEventsLogger, +} from './glean.provider'; +import { mapSession } from './utils/mapSession'; +import { mapUtm } from './utils/mapUtm'; +import { mapSubscription } from './utils/mapSubscription'; +import { mapRelyingParty } from './utils/mapRelyingParty'; +import { normalizeGleanFalsyValues } from './utils/normalizeGleanFalsyValues'; + +@Injectable() +export class PaymentsGleanManager { + constructor( + @Inject(PaymentsGleanProvider) + private paymentsGleanServerEventsLogger: PaymentsGleanServerEventsLogger + ) {} + + async recordFxaPaySetupView( + metricsData: FxaPaySetupViewMetrics, + cmsMetricsData: CmsMetricsData + ) { + const commonMetrics = this.populateCommonMetrics( + metricsData, + cmsMetricsData + ); + + const fxaPaySetupViewMetrics = { + ...commonMetrics, + subscription_checkout_type: metricsData.checkoutType, + }; + + //TODO - Add metrics validation as part of FXA-10135 + + this.paymentsGleanServerEventsLogger.recordPaySetupView( + fxaPaySetupViewMetrics + ); + } + + private populateCommonMetrics( + metricsData: FxaPaySetupViewMetrics, + cmsMetricsData: CmsMetricsData + ) { + return { + user_agent: metricsData.userAgent, + ip_address: metricsData.ipAddress, + account_user_id: normalizeGleanFalsyValues(metricsData.uid), + ...mapRelyingParty(metricsData.searchParams), + ...mapSession(metricsData.searchParams, metricsData.deviceType), + ...mapSubscription({ metricsData, cmsMetricsData }), + ...mapUtm(metricsData.searchParams), + }; + } +} diff --git a/libs/payments/metrics/src/lib/glean/glean.provider.ts b/libs/payments/metrics/src/lib/glean/glean.provider.ts new file mode 100644 index 00000000000..d0220a9554b --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.provider.ts @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { Provider } from '@nestjs/common'; +import { createEventsServerEventLogger } from './__generated__/server_events'; +import { PaymentsGleanConfig } from './glean.config'; + +export type PaymentsGleanServerEventsLogger = ReturnType< + typeof createEventsServerEventLogger +>; +export const PaymentsGleanProvider = Symbol('GleanServerEventsProvider'); + +export const PaymentsGleanFactory: Provider = { + provide: PaymentsGleanProvider, + useFactory: (config: PaymentsGleanConfig) => { + return createEventsServerEventLogger({ + applicationId: config.applicationId, + appDisplayVersion: config.version, + channel: config.channel, + logger_options: { + app: config.loggerAppName, + }, + }); + }, + inject: [PaymentsGleanConfig], +}; + +/** + * Can be used to satisfy DI when unit testing things that should not need + * maxmind. + * Note: this will cause errors to be thrown if geodb is used + */ +export const MockPaymentsGleanFactory = { + provide: PaymentsGleanProvider, + useFactory: () => + ({ + recordPaySetupView: () => {}, + } as any), +} satisfies Provider; diff --git a/libs/payments/metrics/src/lib/glean/glean.service.spec.ts b/libs/payments/metrics/src/lib/glean/glean.service.spec.ts new file mode 100644 index 00000000000..aef7386adfe --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.service.spec.ts @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { Test } from '@nestjs/testing'; +import { + MockStrapiClientConfigProvider, + ProductConfigurationManager, + StrapiClient, +} from '@fxa/shared/cms'; +import { PaymentsGleanManager } from './glean.manager'; +import { PaymentsGleanService } from './glean.service'; +import { + CmsMetricsDataFactory, + FxaPaySetupViewMetricsFactory, + ParamsFactory, +} from './glean.factory'; +import { + MockStripeConfigProvider, + StripeClient, + StripePriceFactory, +} from '@fxa/payments/stripe'; +import { MockStatsDProvider } from '@fxa/shared/metrics/statsd'; +import { PriceManager } from '@fxa/payments/customer'; +import { MockFirestoreProvider } from '@fxa/shared/db/firestore'; +import { MockPaymentsGleanFactory } from './glean.provider'; + +describe('PaymentsGleanService', () => { + let paymentsGleanService: PaymentsGleanService; + let paymentsGleanManager: PaymentsGleanManager; + let productConfigurationManager: ProductConfigurationManager; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + MockPaymentsGleanFactory, + MockStrapiClientConfigProvider, + MockStripeConfigProvider, + MockFirestoreProvider, + MockStatsDProvider, + StrapiClient, + StripeClient, + PriceManager, + PaymentsGleanManager, + ProductConfigurationManager, + PaymentsGleanService, + ], + }).compile(); + + paymentsGleanService = moduleRef.get(PaymentsGleanService); + paymentsGleanManager = moduleRef.get(PaymentsGleanManager); + productConfigurationManager = moduleRef.get(ProductConfigurationManager); + }); + + it('should be defined', () => { + expect(paymentsGleanService).toBeDefined(); + expect(paymentsGleanManager).toBeDefined(); + expect(productConfigurationManager).toBeDefined(); + }); + + describe('handleEventFxaPaySetupView', () => { + const mockCmsData = CmsMetricsDataFactory(); + const mockMetricsData = FxaPaySetupViewMetricsFactory({ + params: ParamsFactory(), + }); + const mockStripePrice = StripePriceFactory({ + id: mockCmsData.priceId, + product: mockCmsData.productId, + }); + + beforeEach(() => { + jest + .spyOn(productConfigurationManager, 'retrieveStripePrice') + .mockResolvedValue(mockStripePrice); + jest + .spyOn(paymentsGleanManager, 'recordFxaPaySetupView') + .mockResolvedValue(); + }); + + it('should call manager record method', async () => { + await paymentsGleanService.handleEventFxaPaySetupView(mockMetricsData); + expect( + productConfigurationManager.retrieveStripePrice + ).toHaveBeenCalledWith( + mockMetricsData.params['offeringId'], + mockMetricsData.params['interval'] + ); + expect(paymentsGleanManager.recordFxaPaySetupView).toHaveBeenCalledWith( + mockMetricsData, + mockCmsData + ); + }); + + it('should call manager record method with empty data', async () => { + jest + .spyOn(productConfigurationManager, 'retrieveStripePrice') + .mockRejectedValue(new Error('fail')); + await paymentsGleanService.handleEventFxaPaySetupView(mockMetricsData); + expect( + productConfigurationManager.retrieveStripePrice + ).toHaveBeenCalledWith( + mockMetricsData.params['offeringId'], + mockMetricsData.params['interval'] + ); + expect(paymentsGleanManager.recordFxaPaySetupView).toHaveBeenCalledWith( + mockMetricsData, + { priceId: '', productId: '' } + ); + }); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/glean.service.ts b/libs/payments/metrics/src/lib/glean/glean.service.ts new file mode 100644 index 00000000000..928d5599201 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.service.ts @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import Emittery from 'emittery'; +import { FxaPaySetupViewMetrics, GleanEvents } from './glean.types'; +import { ProductConfigurationManager } from '@fxa/shared/cms'; +import { Injectable } from '@nestjs/common'; +import { mapParams } from './utils/mapParams'; +import { PaymentsGleanManager } from './glean.manager'; +import { SubplatInterval } from '@fxa/payments/customer'; + +@Injectable() +export class PaymentsGleanService { + private emitter: Emittery; + + constructor( + private productConfigurationManager: ProductConfigurationManager, + private paymentsGleanManager: PaymentsGleanManager + ) { + this.emitter = new Emittery(); + this.emitter.on( + 'fxaPaySetupView', + this.handleEventFxaPaySetupView.bind(this) + ); + } + + getEmitter(): Emittery { + return this.emitter; + } + + async handleEventFxaPaySetupView(metricsData: FxaPaySetupViewMetrics) { + const { offeringId, interval } = mapParams(metricsData.params); + const cmsData = await this.retrieveCMSData(offeringId, interval); + + await this.paymentsGleanManager.recordFxaPaySetupView(metricsData, cmsData); + } + + private async retrieveCMSData(offeringId: string, interval: SubplatInterval) { + try { + const { id: priceId, product: productId } = + await this.productConfigurationManager.retrieveStripePrice( + offeringId, + interval + ); + return { + priceId, + productId, + }; + } catch (error) { + return { + priceId: '', + productId: '', + }; + } + } +} diff --git a/libs/payments/metrics/src/lib/glean/glean.types.ts b/libs/payments/metrics/src/lib/glean/glean.types.ts new file mode 100644 index 00000000000..72e2aa54da5 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/glean.types.ts @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { ResultCart } from '@fxa/payments/cart'; + +type CheckoutType = 'with-accounts' | 'without-accounts'; + +export type CommonMetrics = { + ipAddress: string; + deviceType: string; + userAgent: string; + params: Record; + searchParams: Record; +}; + +export type CartMetrics = Partial< + Pick +> & { + //TODO - Replace on completion of FXA-7584 and pick from ResultCart + currency: string; +}; + +export type FxaPaySetupMetrics = CommonMetrics & CartMetrics; + +export type FxaPaySetupViewMetrics = FxaPaySetupMetrics & { + checkoutType: CheckoutType; +}; + +export type GleanEvents = { + fxaPaySetupView: FxaPaySetupViewMetrics; +}; + +export type CmsMetricsData = { + productId: string; + priceId: string; +}; diff --git a/libs/payments/metrics/src/lib/glean/utils/mapParams.spec.ts b/libs/payments/metrics/src/lib/glean/utils/mapParams.spec.ts new file mode 100644 index 00000000000..4dabd725b45 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapParams.spec.ts @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { mapParams } from './mapParams'; + +describe('mapParams', () => { + it('should map the values if present', () => { + expect( + mapParams({ offeringId: 'offeringId', interval: 'interval' }) + ).toEqual({ + offeringId: 'offeringId', + interval: 'interval', + }); + }); + + it('should return empty strings if the values are not present', () => { + expect(mapParams({})).toEqual({ + offeringId: '', + interval: '', + }); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/utils/mapParams.ts b/libs/payments/metrics/src/lib/glean/utils/mapParams.ts new file mode 100644 index 00000000000..da8ff3e6b46 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapParams.ts @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { SubplatInterval } from '@fxa/payments/customer'; +import { normalizeGleanFalsyValues } from './normalizeGleanFalsyValues'; + +export function mapParams(params: Record) { + const offeringId = normalizeGleanFalsyValues(params['offeringId']); + const interval = normalizeGleanFalsyValues( + params['interval'] + ) as SubplatInterval; + + return { + offeringId, + interval, + }; +} diff --git a/libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.spec.ts b/libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.spec.ts new file mode 100644 index 00000000000..6e369559c5b --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.spec.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { mapRelyingParty } from './mapRelyingParty'; + +describe('mapRelyingParty', () => { + it('should map relying party data if present', () => { + expect(mapRelyingParty({ service: 'vpn' })).toEqual({ + relying_party_oauth_client_id: '', + relying_party_service: 'vpn', + }); + }); + + it('should return empty strings if relying party data is not present', () => { + expect(mapRelyingParty({})).toEqual({ + relying_party_oauth_client_id: '', + relying_party_service: '', + }); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.ts b/libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.ts new file mode 100644 index 00000000000..bc1eb478af3 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapRelyingParty.ts @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { normalizeGleanFalsyValues } from './normalizeGleanFalsyValues'; + +export function mapRelyingParty(searchParams: Record) { + return { + relying_party_oauth_client_id: '', + relying_party_service: normalizeGleanFalsyValues(searchParams['service']), + }; +} diff --git a/libs/payments/metrics/src/lib/glean/utils/mapSession.spec.ts b/libs/payments/metrics/src/lib/glean/utils/mapSession.spec.ts new file mode 100644 index 00000000000..ba047b1d56e --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapSession.spec.ts @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { mapSession } from './mapSession'; + +describe('mapSession', () => { + it('should map the values if present', () => { + const result = mapSession( + { entrypoint: 'entrypoint', flow_id: 'flow id' }, + 'desktop' + ); + expect(result).toEqual({ + session_device_type: 'desktop', + session_entrypoint_experiment: '', + session_entrypoint_variation: '', + session_entrypoint: 'entrypoint', + session_flow_id: 'flow id', + }); + }); + + it('should return empty strings if values are not present', () => { + const result = mapSession({}, 'desktop'); + expect(result).toEqual({ + session_device_type: 'desktop', + session_entrypoint_experiment: '', + session_entrypoint_variation: '', + session_entrypoint: '', + session_flow_id: '', + }); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/utils/mapSession.ts b/libs/payments/metrics/src/lib/glean/utils/mapSession.ts new file mode 100644 index 00000000000..a588bd703ff --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapSession.ts @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { normalizeGleanFalsyValues } from './normalizeGleanFalsyValues'; + +const PLACEHOLDER_FUTURE_USE = ''; + +export function mapSession( + searchParams: Record, + deviceType: string +) { + return { + session_device_type: deviceType, + session_entrypoint_experiment: PLACEHOLDER_FUTURE_USE, + session_entrypoint_variation: PLACEHOLDER_FUTURE_USE, + session_entrypoint: normalizeGleanFalsyValues(searchParams['entrypoint']), + session_flow_id: normalizeGleanFalsyValues(searchParams['flow_id']), + }; +} diff --git a/libs/payments/metrics/src/lib/glean/utils/mapSubscription.spec.ts b/libs/payments/metrics/src/lib/glean/utils/mapSubscription.spec.ts new file mode 100644 index 00000000000..4d5a2f6b9de --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapSubscription.spec.ts @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { CartErrorReasonId } from '@fxa/shared/db/mysql/account'; +import { + CmsMetricsDataFactory, + FxaPaySetupViewMetricsFactory, + ParamsFactory, +} from '../glean.factory'; +import { mapSubscription } from './mapSubscription'; + +describe('mapSubscription', () => { + it('should map all values', () => { + const mockMetricsData = FxaPaySetupViewMetricsFactory({ + params: ParamsFactory(), + couponCode: 'couponCode', + errorReasonId: CartErrorReasonId.BASIC_ERROR, + }); + const mockCmsMetricsData = CmsMetricsDataFactory(); + const result = mapSubscription({ + metricsData: mockMetricsData, + cmsMetricsData: mockCmsMetricsData, + }); + expect(result).toEqual({ + subscription_checkout_type: '', + subscription_currency: mockMetricsData.currency, + subscription_error_id: mockMetricsData.errorReasonId, + subscription_interval: mockMetricsData.params['interval'], + subscription_offering_id: mockMetricsData.params['offeringId'], + subscription_payment_provider: '', + subscription_plan_id: mockCmsMetricsData.priceId, + subscription_product_id: mockCmsMetricsData.productId, + subscription_promotion_code: mockMetricsData.couponCode, + subscription_subscribed_plan_ids: '', + }); + }); + + it('should return empty strings if values are not present', () => { + const mockMetricsData = FxaPaySetupViewMetricsFactory({ + params: {}, + currency: undefined, + errorReasonId: null, + couponCode: null, + }); + const mockCmsMetricsData = CmsMetricsDataFactory({ + productId: undefined, + priceId: undefined, + }); + const result = mapSubscription({ + metricsData: mockMetricsData, + cmsMetricsData: mockCmsMetricsData, + }); + expect(result).toEqual({ + subscription_checkout_type: '', + subscription_currency: '', + subscription_error_id: '', + subscription_interval: '', + subscription_offering_id: '', + subscription_payment_provider: '', + subscription_plan_id: mockCmsMetricsData.priceId, + subscription_product_id: mockCmsMetricsData.productId, + subscription_promotion_code: '', + subscription_subscribed_plan_ids: '', + }); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/utils/mapSubscription.ts b/libs/payments/metrics/src/lib/glean/utils/mapSubscription.ts new file mode 100644 index 00000000000..0026b6e5d05 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapSubscription.ts @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { CmsMetricsData, FxaPaySetupMetrics } from '../glean.types'; +import { mapParams } from './mapParams'; +import { normalizeGleanFalsyValues } from './normalizeGleanFalsyValues'; + +/** + * When necessary, properties populated by PLACEHOLDER_VALUE should + * be provided by the event emitter, and populated by the appropriate + * event handler. + */ +const PLACEHOLDER_VALUE = ''; + +export function mapSubscription({ + metricsData, + cmsMetricsData, +}: { + metricsData: FxaPaySetupMetrics; + cmsMetricsData: CmsMetricsData; +}) { + const mappedParams = mapParams(metricsData.params); + return { + subscription_checkout_type: PLACEHOLDER_VALUE, + subscription_currency: normalizeGleanFalsyValues(metricsData.currency), + subscription_error_id: normalizeGleanFalsyValues(metricsData.errorReasonId), + subscription_interval: mappedParams.interval, + subscription_offering_id: mappedParams.offeringId, + subscription_payment_provider: PLACEHOLDER_VALUE, + subscription_plan_id: cmsMetricsData.priceId, + subscription_product_id: cmsMetricsData.productId, + subscription_promotion_code: normalizeGleanFalsyValues( + metricsData.couponCode + ), + subscription_subscribed_plan_ids: PLACEHOLDER_VALUE, + }; +} diff --git a/libs/payments/metrics/src/lib/glean/utils/mapUtm.spec.ts b/libs/payments/metrics/src/lib/glean/utils/mapUtm.spec.ts new file mode 100644 index 00000000000..504941cc65a --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapUtm.spec.ts @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { mapUtm } from './mapUtm'; + +describe('mapUtm', () => { + it('should map the values if present', () => { + const mockUtmParams = { + utm_source: 'utm_source', + utm_medium: 'utm_medium', + utm_campaign: 'utm_campaign', + utm_content: 'utm_content', + utm_term: 'utm_term', + }; + expect(mapUtm(mockUtmParams)).toEqual(mockUtmParams); + }); + + it('should return empty strings if the values are not present', () => { + expect(mapUtm({})).toEqual({ + utm_campaign: '', + utm_content: '', + utm_medium: '', + utm_source: '', + utm_term: '', + }); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/utils/mapUtm.ts b/libs/payments/metrics/src/lib/glean/utils/mapUtm.ts new file mode 100644 index 00000000000..70200184f01 --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/mapUtm.ts @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { normalizeGleanFalsyValues } from './normalizeGleanFalsyValues'; + +export function mapUtm(searchParams: Record) { + return { + utm_campaign: normalizeGleanFalsyValues(searchParams['utm_campaign']), + utm_content: normalizeGleanFalsyValues(searchParams['utm_content']), + utm_medium: normalizeGleanFalsyValues(searchParams['utm_medium']), + utm_source: normalizeGleanFalsyValues(searchParams['utm_source']), + utm_term: normalizeGleanFalsyValues(searchParams['utm_term']), + }; +} diff --git a/libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.spec.ts b/libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.spec.ts new file mode 100644 index 00000000000..ec6e87ccbaa --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.spec.ts @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { normalizeGleanFalsyValues } from './normalizeGleanFalsyValues'; + +describe('normalizeGleanFalsyValues', () => { + it('should return an empty string if the value is null', () => { + expect(normalizeGleanFalsyValues(null)).toEqual(''); + }); + + it('should return the value if it is not null', () => { + expect(normalizeGleanFalsyValues('value')).toEqual('value'); + }); +}); diff --git a/libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.ts b/libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.ts new file mode 100644 index 00000000000..fcd26c1aa8f --- /dev/null +++ b/libs/payments/metrics/src/lib/glean/utils/normalizeGleanFalsyValues.ts @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +export function normalizeGleanFalsyValues(value?: string | null): string { + return value || ''; +} diff --git a/libs/payments/metrics/tsconfig.json b/libs/payments/metrics/tsconfig.json new file mode 100644 index 00000000000..8122543a9ab --- /dev/null +++ b/libs/payments/metrics/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/payments/metrics/tsconfig.lib.json b/libs/payments/metrics/tsconfig.lib.json new file mode 100644 index 00000000000..4befa7f0990 --- /dev/null +++ b/libs/payments/metrics/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/payments/metrics/tsconfig.spec.json b/libs/payments/metrics/tsconfig.spec.json new file mode 100644 index 00000000000..69a251f328c --- /dev/null +++ b/libs/payments/metrics/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/payments/paypal/src/lib/paypal.client.ts b/libs/payments/paypal/src/lib/paypal.client.ts index 77087f938d1..e013d495ea6 100644 --- a/libs/payments/paypal/src/lib/paypal.client.ts +++ b/libs/payments/paypal/src/lib/paypal.client.ts @@ -289,7 +289,7 @@ export class PayPalClient { REFERENCEID: options.billingAgreementId, }; if (options.cancel) { - data.BILLINGAGREEMENTSTATUS = 'Canceled'; + data['BILLINGAGREEMENTSTATUS'] = 'Canceled'; } return this.doRequest( PaypalMethods.BillAgreementUpdate, diff --git a/libs/payments/paypal/src/lib/util.ts b/libs/payments/paypal/src/lib/util.ts index e075828cc62..4aa9ffcbf09 100644 --- a/libs/payments/paypal/src/lib/util.ts +++ b/libs/payments/paypal/src/lib/util.ts @@ -39,9 +39,9 @@ export function objectToNVP(object: Record): string { const urlSearchParams = new URLSearchParams(santizedObject); // Convert array containing L_ objects to Paypal NVP list string - if (object.L) { + if (object['L']) { urlSearchParams.delete('L'); - object.L.forEach((listItem: any, i: number) => { + object['L'].forEach((listItem: any, i: number) => { Object.keys(listItem).forEach((key) => { urlSearchParams.append(`L_${key}${i}`, listItem[key]); }); diff --git a/libs/payments/ui/src/lib/nestapp/app.module.ts b/libs/payments/ui/src/lib/nestapp/app.module.ts index ad55465b9ad..5e99e0af487 100644 --- a/libs/payments/ui/src/lib/nestapp/app.module.ts +++ b/libs/payments/ui/src/lib/nestapp/app.module.ts @@ -33,6 +33,11 @@ import { AccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account'; import { GeoDBManager, GeoDBNestFactory } from '@fxa/shared/geodb'; import { LocalizerRscFactoryProvider } from '@fxa/shared/l10n/server'; import { StatsDProvider } from '@fxa/shared/metrics/statsd'; +import { + PaymentsGleanManager, + PaymentsGleanFactory, + PaymentsGleanService, +} from '@fxa/payments/metrics'; import { RootConfig } from './config'; import { NextJSActionsService } from './nextjs-actions.service'; @@ -45,7 +50,16 @@ import { validate } from '../config.utils'; // Use the same validate function as apps/payments-next/config // to ensure the same environment variables are loaded following // the same process. - load: () => validate(process.env, RootConfig), + // Note: If just passing in process.env, GLEAN_CONFIG__VERSION + // seems to not be available in validate function. + load: () => + validate( + { + ...process.env, + GLEAN_CONFIG__VERSION: process.env['GLEAN_CONFIG__VERSION'], + }, + RootConfig + ), }), ], controllers: [], @@ -78,6 +92,9 @@ import { validate } from '../config.utils'; StrapiClient, StripeClient, SubscriptionManager, + PaymentsGleanFactory, + PaymentsGleanManager, + PaymentsGleanService, ], }) export class AppModule {} diff --git a/libs/payments/ui/src/lib/nestapp/app.ts b/libs/payments/ui/src/lib/nestapp/app.ts index 322fd077065..7ab6b128c5d 100644 --- a/libs/payments/ui/src/lib/nestapp/app.ts +++ b/libs/payments/ui/src/lib/nestapp/app.ts @@ -10,6 +10,7 @@ import { AppModule } from './app.module'; import { LocalizerRscFactory } from '@fxa/shared/l10n/server'; import { singleton } from '../utils/singleton'; import { NextJSActionsService } from './nextjs-actions.service'; +import { PaymentsGleanService } from '@fxa/payments/metrics'; class AppSingleton { private app!: Awaited< @@ -39,6 +40,10 @@ class AppSingleton { getActionsService() { return this.app.get(NextJSActionsService); } + + getGleanEmitter() { + return this.app.get(PaymentsGleanService).getEmitter(); + } } export async function reinitializeNestApp() { diff --git a/libs/payments/ui/src/lib/nestapp/config.ts b/libs/payments/ui/src/lib/nestapp/config.ts index 4859b72d784..63cb2d06665 100644 --- a/libs/payments/ui/src/lib/nestapp/config.ts +++ b/libs/payments/ui/src/lib/nestapp/config.ts @@ -12,6 +12,7 @@ import { StripeConfig } from '@fxa/payments/stripe'; import { StrapiClientConfig } from '@fxa/shared/cms'; import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config'; import { StatsDConfig } from 'libs/shared/metrics/statsd/src/lib/statsd.config'; +import { PaymentsGleanConfig } from '@fxa/payments/metrics'; export class RootConfig { @Type(() => MySQLConfig) @@ -52,4 +53,9 @@ export class RootConfig { @ValidateNested() @IsDefined() public readonly statsDConfig!: Partial; + + @Type(() => PaymentsGleanConfig) + @ValidateNested() + @IsDefined() + public readonly gleanConfig!: Partial; } diff --git a/libs/shared/db/firestore/src/lib/firestore.provider.ts b/libs/shared/db/firestore/src/lib/firestore.provider.ts index 35b0dd53678..00d33147559 100644 --- a/libs/shared/db/firestore/src/lib/firestore.provider.ts +++ b/libs/shared/db/firestore/src/lib/firestore.provider.ts @@ -23,7 +23,7 @@ export function setupFirestore(config: FirebaseFirestore.Settings) { const testing = !(fsConfig.keyFilename || fsConfig.credentials); // Utilize the local firestore emulator when the env indicates - if (process.env.FIRESTORE_EMULATOR_HOST || testing) { + if (process.env['FIRESTORE_EMULATOR_HOST'] || testing) { return new Firestore({ customHeaders: { Authorization: 'Bearer owner', diff --git a/libs/shared/db/type-cacheable/src/lib/type-cachable-firestore-adapter.ts b/libs/shared/db/type-cacheable/src/lib/type-cachable-firestore-adapter.ts index 15b49b57bba..8ea57cbcaba 100644 --- a/libs/shared/db/type-cacheable/src/lib/type-cachable-firestore-adapter.ts +++ b/libs/shared/db/type-cacheable/src/lib/type-cachable-firestore-adapter.ts @@ -33,7 +33,7 @@ export class FirestoreAdapter implements CacheClient { .get(); const data = result?.data(); - const cachedValue = data?.value; + const cachedValue = data?.['value']; return cachedValue; } diff --git a/libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml b/libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml index 807fd289d8f..32096b8a414 100644 --- a/libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml +++ b/libs/shared/metrics/glean/src/registry/subplat-backend-metrics.yaml @@ -19,20 +19,6 @@ account: - interaction no_lint: - COMMON_PREFIX - user_id_sha256: - type: string - description: | - A hex string of a sha256 hash of the account's uid - lifetime: application - notification_emails: - - subplat-team@mozilla.com - bugs: - - https://mozilla-hub.atlassian.net/browse/FXA-7531 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=tbd - expires: never - data_sensitivity: - - interaction relying_party: service: @@ -40,6 +26,8 @@ relying_party: description: | The service name of the relying party lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -55,6 +43,8 @@ relying_party: description: | The client id of the relying party lifetime: application + send_in_pings: + - events notification_emails: - fxa-staff@mozilla.com bugs: @@ -70,6 +60,8 @@ session: type: string description: an ID generated by FxA for its flow metrics lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -84,6 +76,8 @@ session: type: string description: one of 'mobile', 'tablet', or '' lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -98,6 +92,8 @@ session: type: string description: Entrypoint to the service lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -112,6 +108,8 @@ session: type: string description: Identifier for the experiment the user is part of at the entrypoint lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -126,6 +124,8 @@ session: type: string description: Identifier for the experiment variation the user is part of at the entrypoint lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -143,6 +143,8 @@ subscription: Whether the checkout flow is for new users or existing users. One of “with-account” or “without-account" lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -156,6 +158,8 @@ subscription: type: string description: The third party service ultimately processing a user’s payments (e.g. 'stripe') lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -169,6 +173,8 @@ subscription: type: string description: Product ID of a subscription. lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -182,6 +188,8 @@ subscription: type: string description: Plan ID of a subscription. lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -195,6 +203,8 @@ subscription: type: string description: ID of the offering a customer subscribed to lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -208,6 +218,8 @@ subscription: type: string description: Interval of a subscription used at checkout lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -221,6 +233,8 @@ subscription: type: string description: Currency of a subscription used at checkout lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -234,6 +248,8 @@ subscription: type: string description: The Stripe customer-facing promotion code applied, if any lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -247,6 +263,8 @@ subscription: type: string description: Comma-separated list of Stripe price/plan IDs the user is already subscribed to lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -260,6 +278,8 @@ subscription: type: string description: The error id, if any, encountered for a fxa_pay_setup - fail event lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -281,6 +301,8 @@ utm: special value of 'page+referral+-+not+part+of+a+campaign' is also allowed. type: string lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -301,6 +323,8 @@ utm: in the allowed set of characters. type: string lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -320,6 +344,8 @@ utm: (hyphen) in the allowed set of characters. type: string lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -339,6 +365,8 @@ utm: (hyphen) in the allowed set of characters. type: string lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: @@ -359,6 +387,8 @@ utm: characters. type: string lifetime: application + send_in_pings: + - events notification_emails: - subplat-team@mozilla.com bugs: diff --git a/package.json b/package.json index 92939f2ba54..e6984761217 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "kysely": "^0.27.2", "lint-staged": "^15.2.0", "module-alias": "^2.2.3", + "mozlog": "^3.0.2", "mysql": "^2.18.1", "mysql2": "^3.9.8", "nest-typed-config": "^2.9.2", diff --git a/tsconfig.base.json b/tsconfig.base.json index 806dde27491..0e4d52f63e5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,8 +27,10 @@ "@fxa/payments/capability": ["libs/payments/capability/src/index.ts"], "@fxa/payments/cart": ["libs/payments/cart/src/index.ts"], "@fxa/payments/currency": ["libs/payments/currency/src/index.ts"], + "@fxa/payments/customer": ["libs/payments/customer/src/index.ts"], "@fxa/payments/eligibility": ["libs/payments/eligibility/src/index.ts"], "@fxa/payments/legacy": ["libs/payments/legacy/src/index.ts"], + "@fxa/payments/metrics": ["libs/payments/metrics/src/index.ts"], "@fxa/payments/paypal": ["libs/payments/paypal/src/index.ts"], "@fxa/payments/stripe": ["libs/payments/stripe/src/index.ts"], "@fxa/payments/ui": ["libs/payments/ui/src/index.ts"], @@ -75,8 +77,7 @@ "@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index.ts"], "@fxa/vendored/typesafe-node-firestore": [ "libs/vendored/typesafe-node-firestore/src/index.ts" - ], - "@fxa/payments/customer": ["libs/payments/customer/src/index.ts"] + ] }, "typeRoots": [ "./types", diff --git a/yarn.lock b/yarn.lock index cbc88486dbb..f4b70ab2ec1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41632,6 +41632,7 @@ fsevents@~2.1.1: mocha-junit-reporter: ^2.2.0 mocha-multi: ^1.1.7 module-alias: ^2.2.3 + mozlog: ^3.0.2 mysql: ^2.18.1 mysql2: ^3.9.8 nest-typed-config: ^2.9.2