diff --git a/libs/shared/monitoring/.eslintrc.json b/libs/shared/monitoring/.eslintrc.json new file mode 100644 index 00000000000..3456be9b903 --- /dev/null +++ b/libs/shared/monitoring/.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/shared/monitoring/.swcrc b/libs/shared/monitoring/.swcrc new file mode 100644 index 00000000000..f52b4e44979 --- /dev/null +++ b/libs/shared/monitoring/.swcrc @@ -0,0 +1,14 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + } + } +} diff --git a/libs/shared/monitoring/README.md b/libs/shared/monitoring/README.md new file mode 100644 index 00000000000..ab44e15668f --- /dev/null +++ b/libs/shared/monitoring/README.md @@ -0,0 +1,28 @@ +# shared-monitoring + +This library provides monitoring functionality including error tracking with Sentry and distributed tracing. + +It exports the `initMonitoring` function which initializes both error monitoring (Sentry) and performance monitoring (tracing) for server applications. + +## Usage + +```typescript +import { initMonitoring } from '@fxa/shared/monitoring'; + +initMonitoring({ + log: logger, + config: { + tracing: { + /* tracing config */ + }, + sentry: { + /* sentry config */ + }, + }, +}); +``` + +## Exported APIs + +- `initMonitoring(opts: MonitoringConfig)` - Initialize monitoring components +- `MonitoringConfig` - Type definition for monitoring configuration diff --git a/libs/shared/monitoring/jest.config.ts b/libs/shared/monitoring/jest.config.ts new file mode 100644 index 00000000000..0dd7c87df86 --- /dev/null +++ b/libs/shared/monitoring/jest.config.ts @@ -0,0 +1,42 @@ +import { Config } from 'jest'; +/* 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; + +const config: Config = { + displayName: 'shared-monitoring', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + testEnvironment: 'node', + coverageDirectory: '../../../coverage/libs/shared/monitoring', + reporters: [ + 'default', + [ + 'jest-junit', + { + outputDirectory: 'artifacts/tests/shared-monitoring', + outputName: 'shared-monitoring-jest-unit-results.xml', + }, + ], + ], +}; +export default config; diff --git a/libs/shared/monitoring/package.json b/libs/shared/monitoring/package.json new file mode 100644 index 00000000000..13e95fd4c0a --- /dev/null +++ b/libs/shared/monitoring/package.json @@ -0,0 +1,4 @@ +{ + "name": "@fxa/shared/monitoring", + "version": "0.0.0" +} diff --git a/libs/shared/monitoring/project.json b/libs/shared/monitoring/project.json new file mode 100644 index 00000000000..301e4c336aa --- /dev/null +++ b/libs/shared/monitoring/project.json @@ -0,0 +1,56 @@ +{ + "name": "shared-monitoring", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/monitoring/src", + "projectType": "library", + "tags": ["scope:shared:lib"], + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "main": "libs/shared/monitoring/src/index.ts", + "outputPath": "dist/libs/shared/monitoring", + "outputFileName": "main.js", + "tsConfig": "libs/shared/monitoring/tsconfig.lib.json", + "declaration": true, + "external": [ + "@nestjs/websockets/socket-module", + "@nestjs/microservices/microservices-module", + "@nestjs/microservices" + ], + "assets": [ + { + "glob": "libs/shared/monitoring/README.md", + "input": ".", + "output": "." + } + ], + "platform": "node" + }, + "configurations": { + "development": { + "minify": false + }, + "production": { + "minify": true + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/monitoring/**/*.ts"] + } + }, + "test-unit": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/shared/monitoring/jest.config.ts" + } + } + } +} diff --git a/libs/shared/monitoring/src/index.spec.ts b/libs/shared/monitoring/src/index.spec.ts new file mode 100644 index 00000000000..0da8ac98577 --- /dev/null +++ b/libs/shared/monitoring/src/index.spec.ts @@ -0,0 +1,95 @@ +/* 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 { initTracing } from '@fxa/shared/otel'; +import { initSentry } from '@fxa/shared/sentry-node'; +import { initMonitoring } from './index'; + +jest.mock('@fxa/shared/otel', () => ({ initTracing: jest.fn() })); +jest.mock('@fxa/shared/sentry-node', () => ({ initSentry: jest.fn() })); + +describe('shared-monitoring', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.resetModules(); + }); + + it('initializes tracing and sentry when config contains them', () => { + const log = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + }; + const opts = { + log, + config: { + sentry: { dsn: 'https://example.com' }, + tracing: { + enabled: true, + serviceName: 'test', + batchSpanProcessor: true, + clientName: 'test-client', + corsUrls: 'http://localhost:\\d*/', + filterPii: true, + sampleRate: 1, + batchProcessor: false, + }, + }, + }; + + initMonitoring(opts); + + expect(initTracing).toHaveBeenCalledWith(opts.config.tracing, log); + expect(initSentry).toHaveBeenCalledWith(opts.config, log); + }); + + it('does not call tracing or sentry when not configured', () => { + const log = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + const opts = { log, config: {} }; + + initMonitoring(opts); + + expect(initTracing).not.toHaveBeenCalled(); + expect(initSentry).not.toHaveBeenCalled(); + }); + + it('warns and skips when initialized more than once', () => { + const log = { + warn: jest.fn(), + error: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + }; + const opts = { + log, + config: { + sentry: { dsn: 'https://example.com' }, + tracing: { + enabled: true, + serviceName: 'test', + batchSpanProcessor: true, + clientName: 'test-client', + corsUrls: 'http://localhost:\\d*/', + filterPii: true, + sampleRate: 1, + batchProcessor: false, + }, + }, + }; + + initMonitoring(opts); + initMonitoring(opts); + + expect(log.warn).toHaveBeenCalledWith( + 'monitoring', + 'Monitoring can only be initialized once' + ); + }); +}); diff --git a/packages/fxa-shared/monitoring/index.ts b/libs/shared/monitoring/src/index.ts similarity index 73% rename from packages/fxa-shared/monitoring/index.ts rename to libs/shared/monitoring/src/index.ts index 56e0c9d76d7..914eb73c5e9 100644 --- a/packages/fxa-shared/monitoring/index.ts +++ b/libs/shared/monitoring/src/index.ts @@ -2,19 +2,20 @@ * 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 { initTracing } from '../tracing/node-tracing'; -import { InitSentryOpts, initSentry } from '../sentry/node'; -import { TracingOpts } from '../tracing/config'; -import { ILogger } from '../log'; +import { initTracing } from '@fxa/shared/otel'; +import { initSentry } from '@fxa/shared/sentry-node'; +import { TracingOpts } from '@fxa/shared/otel'; +import { ILogger } from '@fxa/shared/log'; +import { InitSentryOpts } from '@fxa/shared/sentry-utils'; export type MonitoringConfig = { log?: ILogger; - config: InitSentryOpts & { tracing: TracingOpts }; + config: InitSentryOpts & { tracing?: TracingOpts }; }; let initialized = false; -// IMPORTANT! This initialization function must be called first thing when a server starts.If it's called after server +// IMPORTANT! This initialization function must be called first thing when a server starts. If it's called after server // frameworks initialized instrumentation might not work properly. /** * Initializes modules related to error monitoring, performance monitoring, and tracing. diff --git a/libs/shared/monitoring/tsconfig.json b/libs/shared/monitoring/tsconfig.json new file mode 100644 index 00000000000..25f7201d870 --- /dev/null +++ b/libs/shared/monitoring/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/shared/monitoring/tsconfig.lib.json b/libs/shared/monitoring/tsconfig.lib.json new file mode 100644 index 00000000000..e583571eac8 --- /dev/null +++ b/libs/shared/monitoring/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/libs/shared/monitoring/tsconfig.spec.json b/libs/shared/monitoring/tsconfig.spec.json new file mode 100644 index 00000000000..69a251f328c --- /dev/null +++ b/libs/shared/monitoring/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/shared/otel/project.json b/libs/shared/otel/project.json index df6de5a91c6..a611da7ed34 100644 --- a/libs/shared/otel/project.json +++ b/libs/shared/otel/project.json @@ -8,14 +8,30 @@ "build": { "executor": "@nx/esbuild:esbuild", "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", "options": { - "outputPath": "dist/libs/shared/otel", "main": "libs/shared/otel/src/index.ts", + "outputPath": "dist/libs/shared/otel", + "outputFileName": "main.js", "tsConfig": "libs/shared/otel/tsconfig.lib.json", - "assets": ["libs/shared/otel/*.md"], - "generatePackageJson": true, "declaration": true, - "platform": "node" + "assets": [ + { + "glob": "libs/shared/otel/README.md", + "input": ".", + "output": "." + } + ], + "platform": "node", + "format": ["cjs", "esm"] + }, + "configurations": { + "development": { + "minify": false + }, + "production": { + "minify": true + } } }, "test-unit": { diff --git a/libs/shared/sentry-browser/src/lib/browser.ts b/libs/shared/sentry-browser/src/lib/browser.ts index 2298b8586ee..30f19f8edfe 100644 --- a/libs/shared/sentry-browser/src/lib/browser.ts +++ b/libs/shared/sentry-browser/src/lib/browser.ts @@ -35,6 +35,7 @@ export function captureException(err: Error) { ); } }); + Sentry.captureException(err); }); } diff --git a/libs/shared/sentry-utils/src/lib/models/sentry-config-opts.ts b/libs/shared/sentry-utils/src/lib/models/sentry-config-opts.ts index f7fcafca1cc..7dd83e63817 100644 --- a/libs/shared/sentry-utils/src/lib/models/sentry-config-opts.ts +++ b/libs/shared/sentry-utils/src/lib/models/sentry-config-opts.ts @@ -29,6 +29,12 @@ export type SentryConfigOpts = { /** The tracing sample rate. Setting this above 0 will aso result in performance metrics being captured. */ tracesSampleRate?: number; + + /** A function that determines the sample rate for a given event. Setting this will override tracesSampleRate. */ + tracesSampler?: (context: { name?: string }) => number; + + /** A list of errors to ignore. Can be strings, regexes, or functions that take an error and return a boolean. */ + ignoreErrors?: Array; }; }; diff --git a/libs/shared/sentry/src/lib/browser.spec.ts b/libs/shared/sentry/src/lib/browser.spec.ts index 703d5f10150..11f51d05d7e 100644 --- a/libs/shared/sentry/src/lib/browser.spec.ts +++ b/libs/shared/sentry/src/lib/browser.spec.ts @@ -3,12 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import 'jsdom-global/register'; import * as Sentry from '@sentry/browser'; -import sentryMetrics, { _Sentry } from './browser'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; -import { Logger } from './sentry.types'; - -const sinon = require('sinon'); -const sandbox = sinon.createSandbox(); +import * as sentryWrapper from './browser'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; + +jest.mock('@sentry/browser', () => { + const actual = jest.requireActual('@sentry/browser'); + return { + ...actual, + init: jest.fn(), + captureException: jest.fn(), + }; +}); const config: SentryConfigOpts = { release: 'v0.0.0', @@ -19,7 +24,7 @@ const config: SentryConfigOpts = { sampleRate: 0, }, }; -const logger: Logger = { +const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), @@ -27,52 +32,50 @@ const logger: Logger = { }; describe('sentry/browser', () => { - beforeAll(() => { - // Reduce console log noise in test output - sandbox.spy(console, 'error'); - }); - beforeEach(() => { // Make sure it's enabled by default - sentryMetrics.enable(); + sentryWrapper.enable(); }); - afterAll(() => { - sandbox.restore(); + afterEach(() => { + jest.resetAllMocks(); + jest.resetModules(); }); describe('init', () => { it('properly configures with dsn', () => { - sentryMetrics.configure(config, logger); + sentryWrapper.configure(config, logger); }); }); describe('beforeSend', () => { beforeAll(() => { - sentryMetrics.configure(config, logger); + sentryWrapper.configure(config, logger); }); it('works without request url', () => { const data = { key: 'value', - } as Sentry.Event; + type: undefined, + } as Sentry.ErrorEvent; - const resultData = sentryMetrics.__beforeSend(config, data, {}); + const resultData = sentryWrapper.beforeSend(config, data, {}); expect(data).toEqual(resultData); }); it('fingerprints errno', () => { const data = { + type: undefined, request: { url: 'https://example.com', }, tags: { errno: '100', }, - } as Sentry.Event; + } as Sentry.ErrorEvent; - const resultData = sentryMetrics.__beforeSend(config, data, {}); + const resultData = sentryWrapper.beforeSend(config, data, {}); expect(resultData?.fingerprint?.[0]).toEqual('errno100'); expect(resultData?.level).toEqual('info'); }); @@ -80,27 +83,26 @@ describe('sentry/browser', () => { describe('captureException', () => { it('calls Sentry.captureException', () => { - const sentryCaptureException = sinon.stub(_Sentry, 'captureException'); - sentryMetrics.captureException(new Error('testo')); - sinon.assert.calledOnce(sentryCaptureException); - sentryCaptureException.restore(); + const spy = jest.spyOn(Sentry, 'captureException'); + Sentry.captureException(new Error('testo')); + expect(spy).toHaveBeenCalledTimes(1); }); }); describe('disable / enables', () => { it('enables', () => { - sentryMetrics.enable(); - expect(sentryMetrics.__sentryEnabled()).toBeTruthy(); + sentryWrapper.enable(); + expect(sentryWrapper.isEnabled()).toBeTruthy(); }); it('disables', () => { - sentryMetrics.disable(); - expect(sentryMetrics.__sentryEnabled()).toBeFalsy(); + sentryWrapper.disable(); + expect(sentryWrapper.isEnabled()).toBeFalsy(); }); it('will return null from before send when disabled', () => { - sentryMetrics.disable(); - expect(sentryMetrics.__beforeSend({}, {}, {})).toBeNull(); + sentryWrapper.disable(); + expect(sentryWrapper.beforeSend({}, { type: undefined }, {})).toBeNull(); }); }); }); diff --git a/libs/shared/sentry/src/lib/browser.ts b/libs/shared/sentry/src/lib/browser.ts index e9114c9388f..286dc641b9b 100644 --- a/libs/shared/sentry/src/lib/browser.ts +++ b/libs/shared/sentry/src/lib/browser.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as Sentry from '@sentry/browser'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; import { tagFxaName } from './reporting'; import { buildSentryConfig } from './config-builder'; import { Logger } from './sentry.types'; @@ -16,26 +16,59 @@ const EXCEPTION_TAGS = ['code', 'context', 'errno', 'namespace', 'status']; // Internal flag to keep track of whether or not sentry is initialized let sentryEnabled = false; -// HACK: allow tests to stub this function from Sentry -// https://stackoverflow.com/questions/35240469/how-to-mock-the-imports-of-an-es6-module -export const _Sentry = { - captureException: Sentry.captureException, - close: Sentry.close, -}; +export function captureException(err: Error) { + if (!sentryEnabled) { + return; + } + + Sentry.withScope((scope: Sentry.Scope) => { + EXCEPTION_TAGS.forEach((tagName) => { + if (tagName in err) { + scope.setTag( + tagName, + ( + err as { + [key: string]: any; + } + )[tagName] + ); + } + }); + Sentry.captureException(err); + }); +} + +export function isEnabled() { + return sentryEnabled; +} + +/** + * Toggle sentry error capture to be disabled. + */ +export function disable() { + sentryEnabled = false; +} + +/** + * Toggle sentry error capture to be enabled. + */ +export function enable() { + sentryEnabled = true; +} /** - * function that gets called before data gets sent to error metrics + * Gets called before data gets sent to error metrics. Useful for altering event state before it gets sent to Sentry. + * For example, we use this to set the event fingerprint based on known error numbers. * - * @param {Object} event - * Error object data - * @returns {Object} data - * Modified error object data - * @private + * @param {Object} opts - Sentry configuration options + * @param {Object} event - Sentry error object data + * @param {Object} hint - Sentry error object hint data + * @returns {Object} event - Modified error object data */ -function beforeSend( +export function beforeSend( opts: SentryConfigOpts, event: Sentry.ErrorEvent, - hint?: Sentry.EventHint + _hint?: Sentry.EventHint ) { if (sentryEnabled === false) { return null; @@ -57,37 +90,7 @@ function beforeSend( return event; } -function captureException(err: Error) { - if (!sentryEnabled) { - return; - } - - Sentry.withScope((scope: Sentry.Scope) => { - EXCEPTION_TAGS.forEach((tagName) => { - if (tagName in err) { - scope.setTag( - tagName, - ( - err as { - [key: string]: any; - } - )[tagName] - ); - } - }); - _Sentry.captureException(err); - }); -} - -function disable() { - sentryEnabled = false; -} - -function enable() { - sentryEnabled = true; -} - -function configure(config: SentryConfigOpts, log?: Logger) { +export function configure(config: SentryConfigOpts, log?: Logger) { if (!log) { log = console; } @@ -106,7 +109,7 @@ function configure(config: SentryConfigOpts, log?: Logger) { try { Sentry.init({ ...opts, - beforeSend: function (event: Sentry.ErrorEvent, hint: Sentry.EventHint) { + beforeSend: function (event: Sentry.ErrorEvent, hint?: Sentry.EventHint) { return beforeSend(opts, event, hint); }, }); @@ -114,14 +117,3 @@ function configure(config: SentryConfigOpts, log?: Logger) { log.error(e); } } - -export default { - configure, - captureException, - disable, - enable, - __sentryEnabled: function () { - return sentryEnabled; - }, - __beforeSend: beforeSend, -}; diff --git a/libs/shared/sentry/src/lib/config-builder.spec.ts b/libs/shared/sentry/src/lib/config-builder.spec.ts index ce5e2d005fb..798472cd1fe 100644 --- a/libs/shared/sentry/src/lib/config-builder.spec.ts +++ b/libs/shared/sentry/src/lib/config-builder.spec.ts @@ -2,7 +2,7 @@ * 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 { buildSentryConfig } from './config-builder'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; import { Logger } from './sentry.types'; describe('config-builder', () => { diff --git a/libs/shared/sentry/src/lib/config-builder.ts b/libs/shared/sentry/src/lib/config-builder.ts index cd4bf429e5c..49c09babd74 100644 --- a/libs/shared/sentry/src/lib/config-builder.ts +++ b/libs/shared/sentry/src/lib/config-builder.ts @@ -2,7 +2,7 @@ * 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 { Logger } from './sentry.types'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; const sentryEnvMap: Record = { test: 'test', @@ -37,7 +37,6 @@ export function buildSentryConfig(config: SentryConfigOpts, log: Logger) { fxaName: config.sentry?.clientName || config.sentry?.serverName, tracesSampleRate: config.sentry?.tracesSampleRate, ignoreErrors: config.sentry?.ignoreErrors || [], - sendDefaultPii: config.sentry?.sendDefaultPii ?? true, }; return opts; diff --git a/libs/shared/sentry/src/lib/models/SentryConfigOpts.ts b/libs/shared/sentry/src/lib/models/SentryConfigOpts.ts deleted file mode 100644 index 6f60886b8aa..00000000000 --- a/libs/shared/sentry/src/lib/models/SentryConfigOpts.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* 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 { ErrorEvent } from '@sentry/core'; - -export type ExtraOpts = { - integrations?: any[]; - eventFilters?: Array<(event: ErrorEvent, hint: any) => ErrorEvent>; -}; - -export type InitSentryOpts = SentryConfigOpts & ExtraOpts; - -export type SentryConfigOpts = { - /** Name of release */ - release?: string; - - /** Fall back for name of release */ - version?: string; - - /** Sentry specific settings */ - sentry?: { - /** The datasource name. This value can be obtained from the sentry portal. */ - dsn?: string; - /** The environment name. */ - env?: string; - /** The rate (as percent between 0 and 1) at which errors are sampled. Can be reduced to decrease data volume. */ - sampleRate?: number; - /** The name of the active client. */ - clientName?: string; - /** The name of the active server. */ - serverName?: string; - /** The string messages of errors that should be ignored. Strings and regex patterns are permitted. - * When using strings, partial matches will be filtered out. If you need to filter by exact match, use regex patterns instead */ - ignoreErrors?: (string | RegExp)[]; - - /** When set to true, building a configuration will throw an error critical fields are missing. */ - strict?: boolean; - - /** The tracing sample rate. Setting this above 0 will aso result in performance metrics being captured. */ - tracesSampleRate?: number; - - /** Indicates if PII can be transeferred. e.g. Send the IP address. */ - sendDefaultPii?: boolean; - }; -}; diff --git a/libs/shared/sentry/src/lib/next/client.ts b/libs/shared/sentry/src/lib/next/client.ts index 92ff824af7e..e79c6d6756c 100644 --- a/libs/shared/sentry/src/lib/next/client.ts +++ b/libs/shared/sentry/src/lib/next/client.ts @@ -7,7 +7,7 @@ // https://docs.sentry.io/platforms/javascript/guides/nextjs/ import * as Sentry from '@sentry/nextjs'; -import { SentryConfigOpts } from '../models/SentryConfigOpts'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; import { buildSentryConfig } from '../config-builder'; import { Logger } from '../sentry.types'; import { beforeSend } from '../utils/beforeSend.client'; diff --git a/libs/shared/sentry/src/lib/next/server.ts b/libs/shared/sentry/src/lib/next/server.ts index 3be8eacc111..25642e9f4df 100644 --- a/libs/shared/sentry/src/lib/next/server.ts +++ b/libs/shared/sentry/src/lib/next/server.ts @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/nextjs'; import { ErrorEvent } from '@sentry/core'; -import { InitSentryOpts } from '../models/SentryConfigOpts'; +import { InitSentryOpts } from '@fxa/shared/sentry-utils'; import { buildSentryConfig } from '../config-builder'; import { Logger } from '../sentry.types'; import { beforeSend } from '../utils/beforeSend.server'; diff --git a/libs/shared/sentry/src/lib/node.ts b/libs/shared/sentry/src/lib/node.ts index e42848de416..4465b2e5cca 100644 --- a/libs/shared/sentry/src/lib/node.ts +++ b/libs/shared/sentry/src/lib/node.ts @@ -5,7 +5,7 @@ import * as Sentry from '@sentry/node'; import { ErrorEvent } from '@sentry/core'; import { extraErrorDataIntegration } from '@sentry/node'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; import { buildSentryConfig } from './config-builder'; import { tagFxaName } from './reporting'; import { Logger } from './sentry.types'; diff --git a/libs/shared/sentry/src/lib/utils/beforeSend.client.spec.ts b/libs/shared/sentry/src/lib/utils/beforeSend.client.spec.ts index 5c2569ca80a..1bf43f70480 100644 --- a/libs/shared/sentry/src/lib/utils/beforeSend.client.spec.ts +++ b/libs/shared/sentry/src/lib/utils/beforeSend.client.spec.ts @@ -2,7 +2,7 @@ * 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 { SentryConfigOpts } from '../models/SentryConfigOpts'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; import { beforeSend } from './beforeSend.client'; import * as Sentry from '@sentry/nextjs'; diff --git a/libs/shared/sentry/src/lib/utils/beforeSend.client.ts b/libs/shared/sentry/src/lib/utils/beforeSend.client.ts index 0e03f077640..e98a1bbcc04 100644 --- a/libs/shared/sentry/src/lib/utils/beforeSend.client.ts +++ b/libs/shared/sentry/src/lib/utils/beforeSend.client.ts @@ -4,7 +4,7 @@ // Change to @sentry/browser after upgrade to Sentry 8 import * as Sentry from '@sentry/nextjs'; -import { SentryConfigOpts } from '../models/SentryConfigOpts'; +import { SentryConfigOpts } from '@fxa/shared/sentry-utils'; import { tagFxaName } from './tagFxaName'; /** diff --git a/libs/shared/sentry/src/lib/utils/beforeSend.server.ts b/libs/shared/sentry/src/lib/utils/beforeSend.server.ts index 0eab5d21ba6..c3c06e3798e 100644 --- a/libs/shared/sentry/src/lib/utils/beforeSend.server.ts +++ b/libs/shared/sentry/src/lib/utils/beforeSend.server.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { tagFxaName } from './tagFxaName'; -import { InitSentryOpts } from '../models/SentryConfigOpts'; +import { InitSentryOpts } from '@fxa/shared/sentry-utils'; import { ErrorEvent } from '@sentry/core'; const EXPECTED_ERRORS = new Set([ @@ -18,7 +18,7 @@ const EXPECTED_ERRORS = new Set([ 'IntentInsufficientFundsError', 'PayPalPaymentMethodError', 'PayPalServiceUnavailableError', - 'PayPalActiveSubscriptionsMissingAgreementError' + 'PayPalActiveSubscriptionsMissingAgreementError', ]); export const beforeSend = function ( diff --git a/package.json b/package.json index d37d1daffc3..d91c638eaad 100644 --- a/package.json +++ b/package.json @@ -315,6 +315,7 @@ "@fxa/vendored/crypto-relier": "./dist/libs/vendored/crypto-relier/main.cjs", "@fxa/vendored/crypto-relier/esm": "./dist/libs/vendored/crypto-relier/main.js", "@fxa/shared/pem-jwk": "./dist/libs/shared/pem-jwk/main.cjs", - "@fxa/shared/l10n": "./dist/libs/shared/l10n/main.cjs" + "@fxa/shared/l10n": "./dist/libs/shared/l10n/main.cjs", + "@fxa/shared/otel": "./dist/libs/shared/otel/main.cjs" } } diff --git a/packages/fxa-admin-panel/server/config/index.ts b/packages/fxa-admin-panel/server/config/index.ts index e956215fc79..4ba07c0a466 100644 --- a/packages/fxa-admin-panel/server/config/index.ts +++ b/packages/fxa-admin-panel/server/config/index.ts @@ -6,7 +6,7 @@ import convict from 'convict'; import fs from 'fs'; import path from 'path'; import { GuardConfig } from '@fxa/shared/guards'; -import { tracingConfig } from 'fxa-shared/tracing/config'; +import { tracingConfig } from '@fxa/shared/otel'; convict.addFormats(require('convict-format-with-moment')); convict.addFormats(require('convict-format-with-validator')); diff --git a/packages/fxa-admin-panel/server/lib/monitoring.ts b/packages/fxa-admin-panel/server/lib/monitoring.ts index 3322d99ecea..0d8d8ff142f 100644 --- a/packages/fxa-admin-panel/server/lib/monitoring.ts +++ b/packages/fxa-admin-panel/server/lib/monitoring.ts @@ -4,7 +4,7 @@ import Config from '../config'; import mozLog from 'mozlog'; -import { initMonitoring } from 'fxa-shared/monitoring'; +import { initMonitoring } from '@fxa/shared/monitoring'; import { version } from '../../package.json'; const config = Config.getProperties(); diff --git a/packages/fxa-admin-panel/src/index.tsx b/packages/fxa-admin-panel/src/index.tsx index 2b220b63d44..b532bc7830c 100644 --- a/packages/fxa-admin-panel/src/index.tsx +++ b/packages/fxa-admin-panel/src/index.tsx @@ -9,7 +9,7 @@ import AppErrorBoundary from 'fxa-react/components/AppErrorBoundary'; import AppLocalizationProvider from 'fxa-react/lib/AppLocalizationProvider'; import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; -import sentryMetrics from 'fxa-shared/sentry/browser'; +import { initSentry } from '@fxa/shared/sentry-browser'; import { config, readConfigFromMeta, getExtraHeaders } from './lib/config'; import App from './App'; import './styles/tailwind.out.css'; @@ -34,12 +34,15 @@ try { cache: new InMemoryCache(), }); - sentryMetrics.configure({ - release: config.version, - sentry: { - ...config.sentry, + initSentry( + { + release: config.version, + sentry: { + ...config.sentry, + }, }, - }); + console + ); const root = createRoot(document.getElementById('root')!); diff --git a/packages/fxa-auth-server/bin/key_server.js b/packages/fxa-auth-server/bin/key_server.js index 34868f190d2..8c2084fc225 100755 --- a/packages/fxa-auth-server/bin/key_server.js +++ b/packages/fxa-auth-server/bin/key_server.js @@ -21,7 +21,7 @@ const { ProductConfigurationManager, StrapiClient, } = require('@fxa/shared/cms'); -const TracingProvider = require('fxa-shared/tracing/node-tracing'); +const { TracingProvider } = require('@fxa/shared/otel'); const { AppError: error } = require('@fxa/accounts/errors'); const { JWTool } = require('@fxa/vendored/jwtool'); diff --git a/packages/fxa-auth-server/config/index.ts b/packages/fxa-auth-server/config/index.ts index 1574a3cb40b..deae31f4fea 100644 --- a/packages/fxa-auth-server/config/index.ts +++ b/packages/fxa-auth-server/config/index.ts @@ -4,7 +4,7 @@ import convict from 'convict'; import fs from 'fs'; import { makeRedisConfig } from 'fxa-shared/db/config'; -import { tracingConfig } from 'fxa-shared/tracing/config'; +import { tracingConfig } from '@fxa/shared/otel'; import { CloudTasksConvictConfigFactory } from '@fxa/shared/cloud-tasks'; import path from 'path'; import url from 'url'; diff --git a/packages/fxa-auth-server/lib/monitoring.js b/packages/fxa-auth-server/lib/monitoring.js index d03dbb7be3b..aa93277f7a8 100644 --- a/packages/fxa-auth-server/lib/monitoring.js +++ b/packages/fxa-auth-server/lib/monitoring.js @@ -2,7 +2,7 @@ * 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/. */ -const { initMonitoring } = require('fxa-shared/monitoring'); +const { initMonitoring } = require('@fxa/shared/monitoring'); const Sentry = require('@sentry/node'); const { config } = require('../config'); const logger = require('./log')( diff --git a/packages/fxa-auth-server/lib/payments/stripe.ts b/packages/fxa-auth-server/lib/payments/stripe.ts index 852b78de066..ad3359c0a5e 100644 --- a/packages/fxa-auth-server/lib/payments/stripe.ts +++ b/packages/fxa-auth-server/lib/payments/stripe.ts @@ -68,7 +68,7 @@ import { subscriptionProductMetadataValidator } from '../routes/validators'; import { formatMetadataValidationErrorMessage, reportValidationError, -} from 'fxa-shared/sentry/report-validation-error'; +} from '@fxa/shared/sentry-node'; import { AppConfig, AuthFirestore, AuthLogger, TaxAddress } from '../types'; import { PaymentConfigManager } from './configuration/manager'; import { CurrencyHelper } from './currencies'; @@ -3502,7 +3502,10 @@ export class StripeHelper extends StripeHelperBase { return; } - return this.stripeFirestore.fetchAndInsertCustomer(customerId, event.created); + return this.stripeFirestore.fetchAndInsertCustomer( + customerId, + event.created + ); } /** @@ -3525,7 +3528,10 @@ export class StripeHelper extends StripeHelperBase { CUSTOMER_RESOURCE ); if (!customer.deleted && !customer.currency) { - await this.stripeFirestore.fetchAndInsertCustomer(customerId, event.created); + await this.stripeFirestore.fetchAndInsertCustomer( + customerId, + event.created + ); const subscription = await this.stripe.subscriptions.retrieve(subscriptionId); return subscription; @@ -3566,7 +3572,10 @@ export class StripeHelper extends StripeHelperBase { ); } catch (err) { if (err.name === FirestoreStripeError.FIRESTORE_CUSTOMER_NOT_FOUND) { - await this.stripeFirestore.fetchAndInsertCustomer(customerId, event.created); + await this.stripeFirestore.fetchAndInsertCustomer( + customerId, + event.created + ); await this.stripeFirestore.fetchAndInsertInvoice( invoiceId, event.created diff --git a/packages/fxa-auth-server/lib/routes/index.js b/packages/fxa-auth-server/lib/routes/index.js index 2299fc0c7e1..61a2278e313 100644 --- a/packages/fxa-auth-server/lib/routes/index.js +++ b/packages/fxa-auth-server/lib/routes/index.js @@ -5,7 +5,7 @@ 'use strict'; const url = require('url'); -const tracing = require('fxa-shared/tracing/node-tracing'); +const tracing = require('@fxa/shared/otel'); module.exports = function ( log, diff --git a/packages/fxa-auth-server/lib/sentry.js b/packages/fxa-auth-server/lib/sentry.js index 87d444f321f..320894b385b 100644 --- a/packages/fxa-auth-server/lib/sentry.js +++ b/packages/fxa-auth-server/lib/sentry.js @@ -12,7 +12,7 @@ const { ignoreErrors } = require('@fxa/accounts/errors'); const { formatMetadataValidationErrorMessage, reportValidationError, -} = require('fxa-shared/sentry/report-validation-error'); +} = require('@fxa/shared/sentry-node'); function reportSentryMessage(message, captureContext) { Sentry.withScope((scope) => { diff --git a/packages/fxa-auth-server/lib/server.js b/packages/fxa-auth-server/lib/server.js index f9eb3db0ce3..903b0dccfba 100644 --- a/packages/fxa-auth-server/lib/server.js +++ b/packages/fxa-auth-server/lib/server.js @@ -20,9 +20,7 @@ const { configureSentry } = require('./sentry'); const { swaggerOptions } = require('../docs/swagger/swagger-options'); const { Account } = require('fxa-shared/db/models/auth'); const { determineLocale } = require('../../../libs/shared/l10n/src'); -const { - reportValidationError, -} = require('fxa-shared/sentry/report-validation-error'); +const { reportValidationError } = require('@fxa/shared/sentry-node'); const { logErrorWithGlean } = require('./metrics/glean'); const mfa = require('./routes/auth-schemes/mfa'); const verifiedSessionToken = require('./routes/auth-schemes/verified-session-token'); diff --git a/packages/fxa-auth-server/scripts/paypal-processor.ts b/packages/fxa-auth-server/scripts/paypal-processor.ts index 5347db5d97d..dd538c4fc84 100644 --- a/packages/fxa-auth-server/scripts/paypal-processor.ts +++ b/packages/fxa-auth-server/scripts/paypal-processor.ts @@ -13,7 +13,7 @@ import { PayPalHelper } from '../lib/payments/paypal/helper'; import { PayPalClient } from '@fxa/payments/paypal'; import { PaypalProcessor } from '../lib/payments/paypal/processor'; import { setupProcessingTaskObjects } from '../lib/payments/processing-tasks-setup'; -import { initSentry } from 'packages/fxa-shared/sentry/node'; +import { initSentry } from '@fxa/shared/sentry-node'; const pckg = require('../package.json'); const config = require('../config').default.getProperties(); @@ -62,9 +62,8 @@ export async function init() { const lockDuration = parseInt(`${program.lockDuration}`) || DEFAULT_LOCK_DURATION_MS; - const { log, database, senders } = await setupProcessingTaskObjects( - 'paypal-processor' - ); + const { log, database, senders } = + await setupProcessingTaskObjects('paypal-processor'); initSentry( { diff --git a/packages/fxa-auth-server/test/local/server.js b/packages/fxa-auth-server/test/local/server.js index 01869a4023e..222ddfef121 100644 --- a/packages/fxa-auth-server/test/local/server.js +++ b/packages/fxa-auth-server/test/local/server.js @@ -20,7 +20,7 @@ const customs = mocks.mockCustoms(); const sandbox = sinon.createSandbox(); const mockReportValidationError = sandbox.stub(); const server = proxyquire(`${ROOT_DIR}/lib/server`, { - 'fxa-shared/sentry/report-validation-error': { + '@fxa/shared/sentry-node': { reportValidationError: mockReportValidationError, }, }); diff --git a/packages/fxa-content-server/app/scripts/lib/sentry.js b/packages/fxa-content-server/app/scripts/lib/sentry.js index 310b00fd33f..1d0e7ab9ed4 100644 --- a/packages/fxa-content-server/app/scripts/lib/sentry.js +++ b/packages/fxa-content-server/app/scripts/lib/sentry.js @@ -3,11 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as Sentry from '@sentry/browser'; -import { - buildSentryConfig, - tagCriticalEvent, - tagFxaName, -} from 'fxa-shared/sentry'; +import { buildSentryConfig, tagFxaName } from '@fxa/shared/sentry-utils'; import _ from 'underscore'; import Logger from './logger'; @@ -22,7 +18,6 @@ import Logger from './logger'; * @private */ function beforeSend(data) { - data = tagCriticalEvent(data); if (data && data.request) { if (data.tags) { const errno = data.tags.errno; diff --git a/packages/fxa-content-server/app/scripts/models/reliers/oauth.js b/packages/fxa-content-server/app/scripts/models/reliers/oauth.js index bd45acb074f..7d43e9f4485 100644 --- a/packages/fxa-content-server/app/scripts/models/reliers/oauth.js +++ b/packages/fxa-content-server/app/scripts/models/reliers/oauth.js @@ -402,6 +402,7 @@ var OAuthRelier = Relier.extend({ this._wantsScopeThatHasKeys = true; } else { // Requesting keys, but trying to deliver them to an unexpected uri? Nope. + console.warn('Invalid redirect URI' + this.get('redirectUri')); throw new Error('Invalid redirect parameter'); } } diff --git a/packages/fxa-content-server/server/lib/routes/get-update-firefox.js b/packages/fxa-content-server/server/lib/routes/get-update-firefox.js index 65f9a7f92b0..ae4773cf11e 100644 --- a/packages/fxa-content-server/server/lib/routes/get-update-firefox.js +++ b/packages/fxa-content-server/server/lib/routes/get-update-firefox.js @@ -11,9 +11,7 @@ const { logFlowEvent } = require('../flow-event'); const Url = require('url'); const uuid = require('node-uuid'); const validation = require('../validation'); -const { - overrideJoiMessages, -} = require('fxa-shared/sentry/joi-message-overrides'); +const { overrideJoiMessages } = require('@fxa/shared/sentry-node'); const { ACTION: ACTION_TYPE, diff --git a/packages/fxa-content-server/server/lib/routes/post-csp.js b/packages/fxa-content-server/server/lib/routes/post-csp.js index 0b09be849c8..e8d26663ccb 100644 --- a/packages/fxa-content-server/server/lib/routes/post-csp.js +++ b/packages/fxa-content-server/server/lib/routes/post-csp.js @@ -12,9 +12,7 @@ const joi = require('joi'); const logger = require('../logging/log')(); const url = require('url'); const validation = require('../validation'); -const { - overrideJoiMessages, -} = require('fxa-shared/sentry/joi-message-overrides'); +const { overrideJoiMessages } = require('@fxa/shared/sentry-node'); const INTEGER_TYPE = validation.TYPES.INTEGER; const STRING_TYPE = validation.TYPES.LONG_STRING; diff --git a/packages/fxa-content-server/server/lib/routes/post-metrics.js b/packages/fxa-content-server/server/lib/routes/post-metrics.js index 314451b9bed..4be6fed2669 100644 --- a/packages/fxa-content-server/server/lib/routes/post-metrics.js +++ b/packages/fxa-content-server/server/lib/routes/post-metrics.js @@ -12,9 +12,7 @@ const joi = require('joi'); const logger = require('../logging/log')('server.post-metrics'); const MetricsCollector = require('../metrics-collector-stderr'); const validation = require('../validation'); -const { - overrideJoiMessages, -} = require('fxa-shared/sentry/joi-message-overrides'); +const { overrideJoiMessages } = require('@fxa/shared/sentry-node'); const clientMetricsConfig = config.get('client_metrics'); const DISABLE_CLIENT_METRICS_STDERR = diff --git a/packages/fxa-content-server/server/lib/routes/post-nimbus-experiments.js b/packages/fxa-content-server/server/lib/routes/post-nimbus-experiments.js index fb9e3b7e29f..f1f7faf5552 100644 --- a/packages/fxa-content-server/server/lib/routes/post-nimbus-experiments.js +++ b/packages/fxa-content-server/server/lib/routes/post-nimbus-experiments.js @@ -4,9 +4,7 @@ const joi = require('joi'); const Sentry = require('@sentry/node'); -const { - overrideJoiMessages, -} = require('fxa-shared/sentry/joi-message-overrides'); +const { overrideJoiMessages } = require('@fxa/shared/sentry-node'); const BODY_SCHEMA = { client_id: joi.string().required(), @@ -60,7 +58,8 @@ module.exports = function (config, statsd) { statsd.increment('cirrus.experiment-fetch-success'); } } catch (err) { - const isTimeout = err.name === 'AbortError' || err.name === 'TimeoutError'; + const isTimeout = + err.name === 'AbortError' || err.name === 'TimeoutError'; if (statsd) { statsd.increment('cirrus.experiment-fetch-error'); @@ -74,7 +73,7 @@ module.exports = function (config, statsd) { Sentry.captureException(err, { tags: { source: 'nimbus-experiments', - } + }, }); } diff --git a/packages/fxa-content-server/server/lib/routes/redirect-download-firefox.js b/packages/fxa-content-server/server/lib/routes/redirect-download-firefox.js index e1e70461891..f9b7e60d688 100644 --- a/packages/fxa-content-server/server/lib/routes/redirect-download-firefox.js +++ b/packages/fxa-content-server/server/lib/routes/redirect-download-firefox.js @@ -14,9 +14,7 @@ const amplitude = require('../amplitude'); const { logFlowEvent } = require('../flow-event'); const validation = require('../validation'); -const { - overrideJoiMessages, -} = require('fxa-shared/sentry/joi-message-overrides'); +const { overrideJoiMessages } = require('@fxa/shared/sentry-node'); const { ACTION: ACTION_TYPE, diff --git a/packages/fxa-content-server/server/lib/sentry.js b/packages/fxa-content-server/server/lib/sentry.js index a7b33734c0e..41653d56f27 100644 --- a/packages/fxa-content-server/server/lib/sentry.js +++ b/packages/fxa-content-server/server/lib/sentry.js @@ -5,7 +5,7 @@ const Sentry = require('@sentry/node'); const config = require('./configuration'); const RELEASE = require('../../package.json').version; -const { buildSentryConfig, tagFxaName } = require('fxa-shared/sentry'); +const { buildSentryConfig, tagFxaName } = require('@fxa/shared/sentry-utils'); const logger = require('./logging/log')('sentry'); if (config.get('sentry.dsn')) { diff --git a/packages/fxa-content-server/webpack.config.js b/packages/fxa-content-server/webpack.config.js index 9ad8b29f2b2..fcbd3ba66cd 100644 --- a/packages/fxa-content-server/webpack.config.js +++ b/packages/fxa-content-server/webpack.config.js @@ -7,6 +7,7 @@ const webpack = require('webpack'); const path = require('path'); const config = require('./server/lib/configuration').getProperties(); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const ENV = config.env; const webpackConfig = { @@ -56,6 +57,11 @@ const webpackConfig = { path.resolve(__dirname, 'node_modules'), 'node_modules', ], + plugins: [ + new TsconfigPathsPlugin({ + configFile: path.resolve(__dirname, '../../tsconfig.base.json'), + }), + ], alias: { 'chosen-js': require.resolve('chosen-js/public/chosen.jquery'), 'cocktail-lib': require.resolve('backbone.cocktail/Cocktail'), diff --git a/packages/fxa-customs-server/lib/config/config.js b/packages/fxa-customs-server/lib/config/config.js index 51a584bf109..3be74321389 100644 --- a/packages/fxa-customs-server/lib/config/config.js +++ b/packages/fxa-customs-server/lib/config/config.js @@ -2,7 +2,7 @@ * 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/. */ -const { tracingConfig } = require('fxa-shared/tracing/config'); +const { tracingConfig } = require('@fxa/shared/otel'); const { makeRedisConfig } = require('fxa-shared/db/config'); module.exports = function (fs, path, url, convict) { diff --git a/packages/fxa-customs-server/lib/monitoring.js b/packages/fxa-customs-server/lib/monitoring.js index 6f55ba00190..5b2676feb36 100644 --- a/packages/fxa-customs-server/lib/monitoring.js +++ b/packages/fxa-customs-server/lib/monitoring.js @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Sentry = require('@sentry/node'); -const { initMonitoring } = require('fxa-shared/monitoring'); +const { initMonitoring } = require('@fxa/shared/monitoring'); const config = require('./config').getProperties(); const log = require('./log')(config.log.level, 'configure-sentry'); const { version } = require('../package.json'); diff --git a/packages/fxa-customs-server/package.json b/packages/fxa-customs-server/package.json index 16a0e96a7c2..74ba60c5813 100644 --- a/packages/fxa-customs-server/package.json +++ b/packages/fxa-customs-server/package.json @@ -21,7 +21,7 @@ "audit": "npm audit --json | audit-filter --nsp-config=.nsprc --audit=-", "lint": "eslint .", "test": "scripts/test-local.sh", - "test-unit": "yarn make-artifacts-dir && tap test/local --jobs=1 | tap-xunit > ../../artifacts/tests/$npm_package_name/fxa-customs-server-tap-unit-results.xml", + "test-unit": "yarn make-artifacts-dir && tap test/local --node-arg=-r --node-arg=esbuild-register --jobs=1 | tap-xunit > ../../artifacts/tests/$npm_package_name/fxa-customs-server-tap-unit-results.xml", "test-integration": "yarn make-artifacts-dir && tap test/remote --jobs=1 | tap-xunit > ../../artifacts/tests/$npm_package_name/fxa-customs-server-tap-integration-results.xml", "format": "prettier --write --config ../../_dev/.prettierrc '**'", "make-artifacts-dir": "mkdir -p ../../artifacts/tests/$npm_package_name" diff --git a/packages/fxa-customs-server/pm2.config.js b/packages/fxa-customs-server/pm2.config.js index cdc72467d97..943ee0c0dec 100644 --- a/packages/fxa-customs-server/pm2.config.js +++ b/packages/fxa-customs-server/pm2.config.js @@ -10,7 +10,7 @@ module.exports = { apps: [ { name: 'customs', - script: 'node bin/customs_server.js', + script: 'node -r esbuild-register bin/customs_server.js', cwd: __dirname, max_restarts: '1', env: { diff --git a/packages/fxa-customs-server/tsconfig.json b/packages/fxa-customs-server/tsconfig.json new file mode 100644 index 00000000000..1c99a0cf846 --- /dev/null +++ b/packages/fxa-customs-server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "noEmit": true + }, + "include": ["lib/**/*", "test/**/*"] +} diff --git a/packages/fxa-event-broker/jest.config.js b/packages/fxa-event-broker/jest.config.js index 5095839e47b..28ca4d19b11 100644 --- a/packages/fxa-event-broker/jest.config.js +++ b/packages/fxa-event-broker/jest.config.js @@ -1,22 +1,17 @@ const { pathsToModuleNameMapper } = require('ts-jest'); -const { compilerOptions } = require('./tsconfig.build.json'); +const { compilerOptions } = require('../../tsconfig.base.json'); module.exports = { - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), - modulePaths: ['/../dist/'], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + prefix: '/../../../', + }), + modulePaths: ['/../../../'], moduleFileExtensions: ['js', 'json', 'ts'], rootDir: 'src', testRegex: '.spec.ts$', transform: { - '^.+\\.(t|j)s$': [ - 'ts-jest', - { - isolatedModules: true, - }, - ], + '^.+\\.(t|j)s$': 'ts-jest', }, - transformIgnorePatterns: [ - "/node_modules/(?!axios/)", - ], + transformIgnorePatterns: ['/node_modules/(?!axios/)'], coverageDirectory: './coverage', testEnvironment: 'node', }; diff --git a/packages/fxa-event-broker/package.json b/packages/fxa-event-broker/package.json index bc5368f7f5a..837ba79f09c 100644 --- a/packages/fxa-event-broker/package.json +++ b/packages/fxa-event-broker/package.json @@ -84,25 +84,6 @@ "tsconfig-paths": "^4.2.0", "typescript": "5.5.3" }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".spec.ts$", - "transform": { - "^.+\\.(t|j)s$": [ - "ts-jest", - { - "isolatedModules": true - } - ] - }, - "coverageDirectory": "../coverage", - "testEnvironment": "node" - }, "nx": { "tags": [ "scope:broker", diff --git a/packages/fxa-event-broker/src/config.ts b/packages/fxa-event-broker/src/config.ts index df12c3b4747..524274870e0 100644 --- a/packages/fxa-event-broker/src/config.ts +++ b/packages/fxa-event-broker/src/config.ts @@ -5,7 +5,7 @@ import convict from 'convict'; import fs from 'fs'; import path from 'path'; -import { tracingConfig } from 'fxa-shared/tracing/config'; +import { tracingConfig } from '@fxa/shared/otel'; const FIVE_MINUTES = 60 * 5; diff --git a/packages/fxa-event-broker/src/main.ts b/packages/fxa-event-broker/src/main.ts index bec5ff93e42..694fbaaebf0 100644 --- a/packages/fxa-event-broker/src/main.ts +++ b/packages/fxa-event-broker/src/main.ts @@ -14,7 +14,7 @@ import { ConfigService } from '@nestjs/config'; import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import mozLog from 'mozlog'; -import { initTracing } from 'fxa-shared/tracing/node-tracing'; +import { initTracing } from '@fxa/shared/otel'; import { AppModule } from './app.module'; import Config, { AppConfig } from './config'; diff --git a/packages/fxa-event-broker/src/monitoring.ts b/packages/fxa-event-broker/src/monitoring.ts index d1349cc69c7..2e9f02614a5 100644 --- a/packages/fxa-event-broker/src/monitoring.ts +++ b/packages/fxa-event-broker/src/monitoring.ts @@ -4,10 +4,33 @@ import Config, { AppConfig } from './config'; import mozLog from 'mozlog'; -import { initTracing } from 'fxa-shared/tracing/node-tracing'; -import { initSentry } from 'fxa-shared/sentry/node'; +import { initTracing } from '@fxa/shared/otel'; +import { initSentry } from '@fxa/shared/sentry-node'; const log = mozLog(Config.getProperties().log)(Config.getProperties().log.app); const appConfig = Config.getProperties() as AppConfig; initTracing(appConfig.tracing, log); -initSentry(appConfig, log); +initSentry(appConfig, { + error(type: string, data: unknown) { + log.error(type, toObject(data)); + }, + debug(type: string, data: unknown) { + log.debug(type, toObject(data)); + }, + info(type: string, data: unknown) { + log.info(type, toObject(data)); + }, + warn(type: string, data: unknown) { + log.warn(type, toObject(data)); + }, +}); + +function toObject(data: unknown): object { + if (data === null) { + return {}; + } + if (typeof data === 'object') { + return data; + } + return { data }; +} diff --git a/packages/fxa-event-broker/tsconfig.build.json b/packages/fxa-event-broker/tsconfig.build.json index c23732d06f4..64f86c6bd2b 100644 --- a/packages/fxa-event-broker/tsconfig.build.json +++ b/packages/fxa-event-broker/tsconfig.build.json @@ -1,11 +1,4 @@ { "extends": "./tsconfig.json", - "compilerOptions": { - "paths": { - "@fxa/shared/pem-jwk": ["libs/shared/pem-jwk/src/index"], - "@fxa/vendored/jwtool": ["libs/vendored/jwtool/src/index"], - "@fxa/vendored/typesafe-node-firestore": ["libs/vendored/typesafe-node-firestore/src/index"] - } - }, "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] } diff --git a/packages/fxa-event-broker/tsconfig.json b/packages/fxa-event-broker/tsconfig.json index 8ab8bc61d54..96e22777881 100644 --- a/packages/fxa-event-broker/tsconfig.json +++ b/packages/fxa-event-broker/tsconfig.json @@ -9,6 +9,7 @@ "experimentalDecorators": true, "noEmitHelpers": true, "importHelpers": true, + "isolatedModules": true, "types": ["jest"] }, "include": ["src"] diff --git a/packages/fxa-payments-server/server/config/index.js b/packages/fxa-payments-server/server/config/index.js index d96264fad9c..005778e22d7 100644 --- a/packages/fxa-payments-server/server/config/index.js +++ b/packages/fxa-payments-server/server/config/index.js @@ -5,7 +5,7 @@ const fs = require('fs'); const path = require('path'); const convict = require('convict'); -const { tracingConfig } = require('fxa-shared/tracing/config'); +const { tracingConfig } = require('@fxa/shared/otel'); convict.addFormats(require('convict-format-with-moment')); convict.addFormats(require('convict-format-with-validator')); diff --git a/packages/fxa-payments-server/server/jest.config.js b/packages/fxa-payments-server/server/jest.config.js index ec9b15f91ac..b34806625c9 100644 --- a/packages/fxa-payments-server/server/jest.config.js +++ b/packages/fxa-payments-server/server/jest.config.js @@ -16,12 +16,15 @@ module.exports = { }, }, transform: { - "fxa-shared/*": [ "ts-jest", { "isolatedModules": true } ], - "libs/shared/l10n/src": [ "ts-jest", { "isolatedModules": true } ], + '^.+\\.(ts|tsx)$': 'ts-jest', }, // ts-jest - Paths mapping - With helper // https://kulshekhar.github.io/ts-jest/docs/getting-started/paths-mapping#jest-config-with-helper roots: [''], modulePaths: [compilerOptions.baseUrl], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {prefix: '/../../../'}) + moduleNameMapper: { + ...pathsToModuleNameMapper(compilerOptions.paths, {prefix: '/../../../'}), + '^@opentelemetry/otlp-exporter-base/node-http$': '@opentelemetry/otlp-exporter-base/build/src/index-node-http.js', + '^@opentelemetry/otlp-exporter-base/browser-http$': '@opentelemetry/otlp-exporter-base/build/src/index-browser-http.js', + }, }; diff --git a/packages/fxa-payments-server/server/lib/monitoring.js b/packages/fxa-payments-server/server/lib/monitoring.js index c71333f843d..4967533e28c 100644 --- a/packages/fxa-payments-server/server/lib/monitoring.js +++ b/packages/fxa-payments-server/server/lib/monitoring.js @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Sentry = require('@sentry/node'); -const { initMonitoring } = require('fxa-shared/monitoring'); +const { initMonitoring } = require('@fxa/shared/monitoring'); const { config } = require('../config'); const log = require('./logging/log')('configure-sentry'); const { version } = require('../../package.json'); diff --git a/packages/fxa-payments-server/src/lib/sentry.js b/packages/fxa-payments-server/src/lib/sentry.js index 2fcc8b137c4..88b19d35a38 100644 --- a/packages/fxa-payments-server/src/lib/sentry.js +++ b/packages/fxa-payments-server/src/lib/sentry.js @@ -5,11 +5,7 @@ import Logger from './logger'; import * as Sentry from '@sentry/browser'; -import { - tagFxaName, - tagCriticalEvent, - buildSentryConfig, -} from 'fxa-shared/sentry'; +import { tagFxaName, buildSentryConfig } from '@fxa/shared/sentry-utils'; /** * function that gets called before data gets sent to error metrics @@ -79,7 +75,6 @@ SentryMetrics.prototype = { Sentry.init({ ...opts, beforeSend(event) { - event = tagCriticalEvent(event); event = tagFxaName(event, opts.clientName); return event; }, diff --git a/packages/fxa-profile-server/lib/config.js b/packages/fxa-profile-server/lib/config.js index fb6cb7fead2..57b472fb053 100644 --- a/packages/fxa-profile-server/lib/config.js +++ b/packages/fxa-profile-server/lib/config.js @@ -5,15 +5,13 @@ const fs = require('fs'); const path = require('path'); -const tracing = require('fxa-shared/tracing/config'); +const { tracingConfig } = require('@fxa/shared/otel'); const convict = require('convict'); const convict_format_with_validator = require('convict-format-with-validator'); const convict_format_with_moment = require('convict-format-with-moment'); convict.addFormats(convict_format_with_validator); convict.addFormats(convict_format_with_moment); -const tracingConfig = tracing.tracingConfig; - const conf = convict({ api: { version: { diff --git a/packages/fxa-profile-server/lib/monitoring.js b/packages/fxa-profile-server/lib/monitoring.js index 3ea95c1d3f7..5e87a394840 100644 --- a/packages/fxa-profile-server/lib/monitoring.js +++ b/packages/fxa-profile-server/lib/monitoring.js @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Sentry = require('@sentry/node'); -const { initMonitoring } = require('fxa-shared/monitoring'); +const { initMonitoring } = require('@fxa/shared/monitoring'); const config = require('./config').getProperties(); const log = require('./logging')('configure-sentry'); const { version } = require('../package.json'); diff --git a/packages/fxa-profile-server/package.json b/packages/fxa-profile-server/package.json index a2b9621309f..9e9b4a4fee6 100644 --- a/packages/fxa-profile-server/package.json +++ b/packages/fxa-profile-server/package.json @@ -14,7 +14,7 @@ "restart": "pm2 restart pm2.config.js", "delete": "pm2 delete pm2.config.js", "test": "scripts/test-local.sh", - "test-unit": "MOCHA_FILE=../../artifacts/tests/$npm_package_name/fxa-profile-server-mocha-unit-results.xml node ./scripts/mocha-coverage.js --recursive test/*.js test/routes/*/*.js -g '#integration' --invert", + "test-unit": "MOCHA_FILE=../../artifacts/tests/$npm_package_name/fxa-profile-server-mocha-unit-results.xml node -r esbuild-register ./scripts/mocha-coverage.js --recursive test/*.js test/routes/*/*.js -g '#integration' --invert", "test-integration": "MOCHA_FILE=../../artifacts/tests/$npm_package_name/fxa-profile-server-mocha-integration-results.xml node ./scripts/mocha-coverage.js --recursive test/*.js test/routes/*/*.js -g '#integration'", "format": "prettier --write --config ../../_dev/.prettierrc '**'" }, diff --git a/packages/fxa-profile-server/pm2.config.js b/packages/fxa-profile-server/pm2.config.js index cfb3d6dbbcd..20be80d7762 100644 --- a/packages/fxa-profile-server/pm2.config.js +++ b/packages/fxa-profile-server/pm2.config.js @@ -10,7 +10,7 @@ module.exports = { apps: [ { name: 'profile', - script: 'node bin/server.js', + script: 'node -r esbuild-register bin/server.js', cwd: __dirname, max_restarts: '1', env: { @@ -30,7 +30,7 @@ module.exports = { }, { name: 'profile-worker', - script: 'node bin/worker.js', + script: 'node -r esbuild-register bin/worker.js', cwd: __dirname, max_restarts: '1', env: { @@ -46,7 +46,7 @@ module.exports = { }, { name: 'profile-static', - script: 'node bin/_static.js', + script: 'node -r esbuild-register bin/_static.js', cwd: __dirname, max_restarts: '1', env: { diff --git a/packages/fxa-profile-server/scripts/mocha-coverage.js b/packages/fxa-profile-server/scripts/mocha-coverage.js index 9a405e4154b..3b0e5532367 100755 --- a/packages/fxa-profile-server/scripts/mocha-coverage.js +++ b/packages/fxa-profile-server/scripts/mocha-coverage.js @@ -28,6 +28,8 @@ const argv = [ '--reporter=text', '--report-dir=coverage', MOCHA_BIN, + '--require', + 'module-alias/register', ]; const arg = argv.concat(process.argv.slice(2)); diff --git a/packages/fxa-profile-server/tsconfig.json b/packages/fxa-profile-server/tsconfig.json new file mode 100644 index 00000000000..af6e78c30e8 --- /dev/null +++ b/packages/fxa-profile-server/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "allowJs": true, + "checkJs": false, + "outDir": "./dist", + "types": ["mocha", "mozlog", "node"], + "lib": ["ESNext"], + "noImplicitAny": false + }, + "include": [ + "bin/*", + "lib/**/*", + "scripts/**/*", + "test/**/*" + ] +} diff --git a/packages/fxa-settings/src/components/App/index.test.tsx b/packages/fxa-settings/src/components/App/index.test.tsx index 727f0fff536..f11d1c2aab9 100644 --- a/packages/fxa-settings/src/components/App/index.test.tsx +++ b/packages/fxa-settings/src/components/App/index.test.tsx @@ -33,7 +33,7 @@ import { currentAccount } from '../../lib/cache'; import { MozServices } from '../../lib/types'; import mockUseFxAStatus from '../../lib/hooks/useFxAStatus/mocks'; import useFxAStatus from '../../lib/hooks/useFxAStatus'; -import sentryMetrics from 'fxa-shared/sentry/browser'; +import * as sentryWrapper from '@fxa/shared/sentry-browser'; import { OAuthError } from '../../lib/oauth'; jest.mock('../../lib/hooks/useFxAStatus', () => ({ @@ -41,7 +41,7 @@ jest.mock('../../lib/hooks/useFxAStatus', () => ({ default: jest.fn(), })); -jest.mock('fxa-shared/sentry/browser', () => ({ +jest.mock('@fxa/shared/sentry/browser', () => ({ __esModule: true, default: { enable: jest.fn(), @@ -653,7 +653,7 @@ describe('Integration serviceName error handling', () => { (useLocalSignedInQueryState as jest.Mock).mockRestore(); (useSession as jest.Mock).mockRestore(); (currentAccount as jest.Mock).mockRestore(); - (sentryMetrics.captureException as jest.Mock).mockClear(); + (sentryWrapper.captureException as jest.Mock).mockClear(); }); it('shows OAuthDataError component when OAuth integration throws', async () => { diff --git a/packages/fxa-settings/src/components/App/index.tsx b/packages/fxa-settings/src/components/App/index.tsx index 28a1eabe5a3..eb39a8c980c 100644 --- a/packages/fxa-settings/src/components/App/index.tsx +++ b/packages/fxa-settings/src/components/App/index.tsx @@ -38,7 +38,7 @@ import { } from '../../models/contexts/SettingsContext'; import { AccountStateProvider } from '../../models/contexts/AccountStateContext'; -import sentryMetrics from 'fxa-shared/sentry/browser'; +import { disableSentry, enableSentry } from '@fxa/shared/sentry-utils'; import { maybeRecordWebAuthnCapabilities } from '../../lib/webauthnCapabilitiesProbe'; // Components @@ -174,7 +174,7 @@ export const App = ({ // - we can't send any identifying metrics to sentry // - we can't determine whether or not they have opted out if (isSignedInData === undefined || isSignedInData.isSignedIn === false) { - sentryMetrics.enable(); + enableSentry(); } const config = useConfig(); @@ -309,7 +309,8 @@ export const App = ({ recoveryKey: data.account.recoveryKey?.exists ?? false, hasSecondaryVerifiedEmail: data.account.emails.length > 1 && data.account.emails[1].verified, - totpActive: (data.account.totp?.exists && data.account.totp?.verified) ?? false, + totpActive: + (data.account.totp?.exists && data.account.totp?.verified) ?? false, }); } }, [ @@ -328,10 +329,10 @@ export const App = ({ useEffect(() => { if (metricsEnabled || isSignedIn === false) { - sentryMetrics.enable(); + enableSentry(); maybeRecordWebAuthnCapabilities(); } else { - sentryMetrics.disable(); + disableSentry(); } }, [ data?.account?.metricsEnabled, diff --git a/packages/fxa-settings/src/index.tsx b/packages/fxa-settings/src/index.tsx index 81423abf1d2..fd45c53febc 100644 --- a/packages/fxa-settings/src/index.tsx +++ b/packages/fxa-settings/src/index.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { render } from 'react-dom'; -import sentryMetrics from 'fxa-shared/sentry/browser'; +import * as sentryWrapper from '@fxa/shared/sentry-browser'; import { AppErrorBoundary } from './components/ErrorBoundaries'; import App from './components/App'; import { NimbusProvider } from './models/contexts/NimbusContext'; @@ -51,7 +51,7 @@ try { }); // Must be configured early. Otherwise baggage and sentry-trace headers won't be added - sentryMetrics.configure({ + sentryWrapper.initSentry({ release: config.version, sentry: { ...config.sentry, diff --git a/packages/fxa-settings/src/lib/metrics.ts b/packages/fxa-settings/src/lib/metrics.ts index 30ddec4507b..7f58ee617ae 100644 --- a/packages/fxa-settings/src/lib/metrics.ts +++ b/packages/fxa-settings/src/lib/metrics.ts @@ -2,7 +2,7 @@ * 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 sentryMetrics from 'fxa-shared/sentry/browser'; +import * as sentryWrapper from '@fxa/shared/sentry-browser'; import { window } from './window'; import { v4 as uuid } from 'uuid'; import { QueryParams } from '..'; @@ -323,7 +323,7 @@ export function logEvents( ); } catch (e) { console.error('AppError', e); - sentryMetrics.captureException(e); + sentryWrapper.captureException(e); } } diff --git a/packages/fxa-settings/src/models/integrations/oauth-web-integration.ts b/packages/fxa-settings/src/models/integrations/oauth-web-integration.ts index 7ca671c8894..4a2c6d7e44c 100644 --- a/packages/fxa-settings/src/models/integrations/oauth-web-integration.ts +++ b/packages/fxa-settings/src/models/integrations/oauth-web-integration.ts @@ -212,6 +212,7 @@ export class OAuthWebIntegration extends GenericIntegration< wantsScopeThatHasKeys = true; } else { // Requesting keys, but trying to deliver them to an unexpected uri? Nope. + console.warn('Invalid redirect URI:' + this.clientInfo?.redirectUri); throw new Error('Invalid redirect parameter'); } } diff --git a/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/container.test.tsx b/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/container.test.tsx index a73b024f760..4d8e90e887c 100644 --- a/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/container.test.tsx +++ b/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/container.test.tsx @@ -9,7 +9,7 @@ import * as HooksModule from '../../../lib/oauth/hooks'; import * as OAuthFlowRecoveryModule from '../../../lib/hooks/useOAuthFlowRecovery'; import * as CacheModule from '../../../lib/cache'; import * as ReachRouterModule from '@reach/router'; -import * as SentryModule from 'fxa-shared/sentry/browser'; +import * as SentryModule from '@fxa/shared/sentry-browser'; import * as ReactUtils from 'fxa-react/lib/utils'; import { screen, waitFor } from '@testing-library/react'; @@ -143,7 +143,7 @@ function applyMocks() { }); mockLocation(); mockReactUtilsModule(); - jest.spyOn(SentryModule.default, 'captureException'); + jest.spyOn(SentryModule, 'captureException'); jest.spyOn(OAuthFlowRecoveryModule, 'useOAuthFlowRecovery').mockReturnValue({ isRecovering: false, recoveryFailed: false, @@ -326,7 +326,9 @@ describe('confirm-signup-container', () => { .mockReturnValue({ isRecovering: false, recoveryFailed: true, - attemptOAuthFlowRecovery: jest.fn().mockResolvedValue({ success: false }), + attemptOAuthFlowRecovery: jest + .fn() + .mockResolvedValue({ success: false }), }); render(); @@ -334,7 +336,8 @@ describe('confirm-signup-container', () => { await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith('/signin', { state: { - localizedErrorMessage: 'Something went wrong. Please sign in again.', + localizedErrorMessage: + 'Something went wrong. Please sign in again.', }, }); }); diff --git a/packages/fxa-shared/README.md b/packages/fxa-shared/README.md index 47aedeb2742..9979cda3820 100644 --- a/packages/fxa-shared/README.md +++ b/packages/fxa-shared/README.md @@ -93,7 +93,7 @@ Or by building up the new set in place: This utility allows for easy configuration of tracing. To use this in a service: -- Add the config chunk in fxa-shared/tracing/config to your packages's config. +- Add the config chunk in @fxa/shared/otel to your packages's config. - Then initialize as follows. This invocation should happen as early as possible in the service's or app's lifecycle. Ideally it's the first thing done, even before importing other modules. @@ -101,7 +101,7 @@ This utility allows for easy configuration of tracing. To use this in a service: ``` // For services const config = require('../config'); -require('fxa-shared/tracing/node-tracing').initTracing(config.get('tracing')); +require('@fxa/shared/otel').initTracing(config.get('tracing')); ``` To see traces on your local system, set the following environment variables: @@ -122,7 +122,7 @@ TRACING_OTEL_COLLECTOR_JAEGER_ENABLED=true ``` -The default config for tracing found at fxa-shared/tracing/config.ts will pick up these variables and result in traces showing up in Jaeger which runs locally at `localhost:16686`. +The default config for tracing found at @fxa/shared/otel will pick up these variables and result in traces showing up in Jaeger which runs locally at `localhost:16686`. It's important to note that sentry also supports tracing integration. So we typically let a call to 'initSentry', a function located in the sentry/node.ts module do the work of initializing tracing. diff --git a/packages/fxa-shared/package.json b/packages/fxa-shared/package.json index eb9e93c16f1..83c2bfde434 100644 --- a/packages/fxa-shared/package.json +++ b/packages/fxa-shared/package.json @@ -121,10 +121,6 @@ "import": "./dist/esm/packages/fxa-shared/log/*.js", "require": "./dist/cjs/packages/fxa-shared/log/*.js" }, - "./monitoring": { - "import": "./dist/esm/packages/fxa-shared/monitoring/index.js", - "require": "./dist/cjs/packages/fxa-shared/monitoring/index.js" - }, "./metrics/*": { "import": "./dist/esm/packages/fxa-shared/metrics/*.js", "require": "./dist/cjs/packages/fxa-shared/metrics/*.js" @@ -169,14 +165,6 @@ "import": "./dist/esm/packages/fxa-shared/scripts/*.js", "require": "./dist/cjs/packages/fxa-shared/scripts/*.js" }, - "./sentry": { - "import": "./dist/esm/packages/fxa-shared/sentry/index.js", - "require": "./dist/cjs/packages/fxa-shared/sentry/index.js" - }, - "./sentry/*": { - "import": "./dist/esm/packages/fxa-shared/sentry/*.js", - "require": "./dist/cjs/packages/fxa-shared/sentry/*.js" - }, "./speed-trap/*": { "import": "./dist/esm/packages/fxa-shared/speed-trap/*.js", "require": "./dist/cjs/packages/fxa-shared/speed-trap/*.js" @@ -185,10 +173,6 @@ "import": "./dist/esm/packages/fxa-shared/subscriptions/*.js", "require": "./dist/cjs/packages/fxa-shared/subscriptions/*.js" }, - "./tracing/*": { - "import": "./dist/esm/packages/fxa-shared/tracing/*.js", - "require": "./dist/cjs/packages/fxa-shared/tracing/*.js" - }, "./test/db/models/auth/*": { "import": "./dist/esm/packages/fxa-shared/test/db/models/auth/*.js", "require": "./dist/cjs/packages/fxa-shared/test/db/models/auth/*.js" diff --git a/packages/fxa-shared/sentry/browser.ts b/packages/fxa-shared/sentry/browser.ts deleted file mode 100644 index 2d23b6b1586..00000000000 --- a/packages/fxa-shared/sentry/browser.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* 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 * as Sentry from '@sentry/browser'; -import { Integration } from '@sentry/core'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; -import { ILogger } from '../log'; -import { tagFxaName } from './tag'; -import { buildSentryConfig } from './config-builder'; - -/** - * Exception fields that are imported as tags - */ -const EXCEPTION_TAGS = ['code', 'context', 'errno', 'namespace', 'status']; - -// Internal flag to keep track of whether or not sentry is initialized -let sentryEnabled = false; - -// HACK: allow tests to stub this function from Sentry -// https://stackoverflow.com/questions/35240469/how-to-mock-the-imports-of-an-es6-module -export const _Sentry = { - captureException: Sentry.captureException, - close: Sentry.close, -}; - -/** - * function that gets called before data gets sent to error metrics - * - * @param {Object} event - * Error object data - * @returns {Object} data - * Modified error object data - * @private - */ -function beforeSend( - opts: SentryConfigOpts, - event: Sentry.ErrorEvent, - hint?: Sentry.EventHint -) { - if (sentryEnabled === false) { - return null; - } - - if (event.request) { - if (event.tags) { - // if this is a known errno, then use grouping with fingerprints - // Docs: https://docs.sentry.io/hosted/learn/rollups/#fallback-grouping - if (event.tags.errno) { - event.fingerprint = ['errno' + (event.tags.errno as number)]; - // if it is a known error change the error level to info. - event.level = 'info'; - } - } - } - - event = tagFxaName(event, opts.sentry?.clientName || opts.sentry?.serverName); - return event; -} - -function captureException(err: Error) { - if (!sentryEnabled) { - return; - } - - Sentry.withScope((scope: Sentry.Scope) => { - EXCEPTION_TAGS.forEach((tagName) => { - if (tagName in err) { - scope.setTag( - tagName, - ( - err as { - [key: string]: any; - } - )[tagName] - ); - } - }); - _Sentry.captureException(err); - }); -} - -function disable() { - sentryEnabled = false; -} - -function enable() { - sentryEnabled = true; -} - -function configure(config: SentryConfigOpts, log?: ILogger) { - if (!log) { - log = console; - } - - if (!config?.sentry?.dsn) { - log.error('sentry.dsn.missing', 'No Sentry dsn provided'); - return; - } - - // We want sentry to be disabled by default... This is because we only emit data - // for users that 'have opted in'. A subsequent call to 'enable' is needed to ensure - // that sentry events only flow under the proper circumstances. - disable(); - - const opts = buildSentryConfig(config, log); - - // If tracing is configured, add the integration. - const integrations: Integration[] = []; - if (opts.tracesSampleRate || opts.tracesSampler) { - integrations.push(Sentry.browserTracingIntegration()); - } - - try { - Sentry.init({ - ...opts, - beforeSend: function (event: Sentry.ErrorEvent, hint?: Sentry.EventHint) { - return beforeSend(opts, event, hint); - }, - integrations, - }); - } catch (e) { - log.error(e); - } -} - -export default { - configure, - captureException, - disable, - enable, - __sentryEnabled: function () { - return sentryEnabled; - }, - __beforeSend: beforeSend, -}; diff --git a/packages/fxa-shared/sentry/config-builder.ts b/packages/fxa-shared/sentry/config-builder.ts deleted file mode 100644 index 055b419c7fc..00000000000 --- a/packages/fxa-shared/sentry/config-builder.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* 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 { ILogger } from '../log'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; - -const sentryEnvMap: Record = { - test: 'test', - local: 'local', - dev: 'dev', - ci: 'ci', - stage: 'stage', - prod: 'prod', - production: 'prod', - development: 'dev', -}; - -function toEnv(val: any) { - if (typeof val === 'string') { - return sentryEnvMap[val] || ''; - } - return ''; -} - -export function buildSentryConfig(config: SentryConfigOpts, log: ILogger) { - if (log) { - checkSentryConfig(config, log); - } - - const opts = { - dsn: config.sentry?.dsn || '', - release: config.release || config.version, - environment: toEnv(config.sentry?.env), - sampleRate: config.sentry?.sampleRate, - clientName: config.sentry?.clientName, - serverName: config.sentry?.serverName, - fxaName: config.sentry?.clientName || config.sentry?.serverName, - tracesSampleRate: config.sentry?.tracesSampleRate, - tracesSampler: config.sentry?.tracesSampler, - }; - - return opts; -} - -function checkSentryConfig(config: SentryConfigOpts, log: ILogger) { - if (!config || !config.sentry || !config.sentry?.dsn) { - raiseError('sentry.dsn not specified. sentry disabled.'); - return; - } else { - log?.info('sentry-config-builder', { - msg: `Config setting for sentry.dsn specified, enabling sentry for env ${config.sentry.env}!`, - }); - } - - if (!config.sentry.env) { - raiseError('config missing either environment or env.'); - } else if (!toEnv(config.sentry.env)) { - raiseError( - `invalid config.env. ${config.sentry.env} options are: ${Object.keys( - sentryEnvMap - ).join(',')}` - ); - } else { - log?.info('sentry-config-builder', { - msg: 'sentry targeting: ' + sentryEnvMap[config.sentry.env], - }); - } - - if (!config.release && !config.version) { - raiseError('config missing either release or version.'); - } - - if (config.sentry?.sampleRate == null) { - raiseError('config missing sentry.sampleRate'); - } - if (!config.sentry.clientName && !config.sentry.serverName) { - raiseError('config missing either sentry.clientName or sentry.serverName'); - } - - function raiseError(msg: string) { - log?.warn('sentry-config-builder', { msg }); - if (config.sentry?.strict) { - throw new SentryConfigurationBuildError(msg); - } - } -} - -class SentryConfigurationBuildError extends Error {} diff --git a/packages/fxa-shared/sentry/index.ts b/packages/fxa-shared/sentry/index.ts deleted file mode 100644 index 48f8f605566..00000000000 --- a/packages/fxa-shared/sentry/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './tag'; -export * from './config-builder'; -export * from './models/SentryConfigOpts'; diff --git a/packages/fxa-shared/sentry/joi-message-overrides.ts b/packages/fxa-shared/sentry/joi-message-overrides.ts deleted file mode 100644 index 08cb4c44588..00000000000 --- a/packages/fxa-shared/sentry/joi-message-overrides.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* 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 { AnySchema } from 'joi'; - -/** - * A set of default message overrides. These result in better error resolution in sentry. - */ -export const defaultMessageOverrides = { - // Override some of the message defaults. Here we remove the 'with value {:[.]}' - // portion of the message, because it causes too much fragmentation in our sentry - // errors. These should be applied to any .regex or .pattern joi validator. - // Form more context concerning overriding messages see: - // - https://joi.dev/api/?v=17.6.0#anymessagesmessages - // - https://github.com/hapijs/joi/blob/7aa36666863c1dde7e4eb02a8058e00555a99d54/lib/types/string.js#L718 - 'string.pattern.base': - '{{#label}} fails to match the required pattern: {{#regex}}', - 'string.pattern.name': '{{#label}} fails to match the {{#name}} pattern', - 'string.pattern.invert.base': - '{{#label}} matches the inverted pattern: {{#regex}}', - 'string.pattern.invert.name': - '{{#label}} matches the inverted {{#name}} pattern', -}; - -/** - * Applies a set of message overrides to the default joi message formats. - * @param data - Set of joi validators to apply message overrides to to. Note, data is mutated. - * @param overrides - Set of optional overrides, if none are provide the defaultMessageOverrides are used. - * @returns data - */ -export function overrideJoiMessages( - data: Record, - overrides?: Record -) { - Object.keys(data).forEach( - (x) => (data[x] = data[x].messages(overrides || defaultMessageOverrides)) - ); - return data; -} diff --git a/packages/fxa-shared/sentry/models/SentryConfigOpts.ts b/packages/fxa-shared/sentry/models/SentryConfigOpts.ts deleted file mode 100644 index 5534a38491e..00000000000 --- a/packages/fxa-shared/sentry/models/SentryConfigOpts.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* 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 { SamplingContext } from '@sentry/core'; - -export type SentryConfigOpts = { - /** Name of release */ - release?: string; - - /** Fall back for name of release */ - version?: string; - - /** Sentry specific settings */ - sentry?: { - /** The datasource name. This value can be obtained from the sentry portal. */ - dsn?: string; - /** The environment name. */ - env?: string; - /** The rate (as percent between 0 and 1) at which errors are sampled. Can be reduced to decrease data volume. */ - sampleRate?: number; - /** The name of the active client. */ - clientName?: string; - /** The name of the active server. */ - serverName?: string; - - /** When set to true, building a configuration will throw an error critical fields are missing. */ - strict?: boolean; - - /** The tracing sample rate. Setting this above 0 will aso result in performance metrics being captured. */ - tracesSampleRate?: number; - - /** Call back to dynamically determine sampling rate. When defined, tracesSampleRate won't be used. */ - tracesSampler?: (context: SamplingContext) => number | boolean; - }; -}; diff --git a/packages/fxa-shared/sentry/models/pii.ts b/packages/fxa-shared/sentry/models/pii.ts deleted file mode 100644 index 349cbbcc7b9..00000000000 --- a/packages/fxa-shared/sentry/models/pii.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* 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/. */ - -/** A general type that holds PII data. */ -export type PiiData = Record | string | undefined | null; - -/** - * Result of a filter action. - */ -export interface FilterActionResult { - /** - * The modified value - */ - val: T; - - /** - * Whether or not the pipeline can be exited. In the event the filter removes enough data, it might - * make sense to exit the pipeline of filter actions early. - */ - exitPipeline: boolean; -} - -/** A general interface for running a filter action on PII Data */ -export interface IFilterAction { - /** - * Filters a value for PII - * @param val - the value to filter - * @param depth - if filtering an object, the depth of the current traversal - * @returns the provided value with modifications, and flag if the action pipeline can be exited. - */ - execute(val: T, depth?: number): FilterActionResult; -} - -/** A general interface for top level classes that filter PII data */ -export interface IFilter { - filter(event: PiiData): PiiData; -} - -/** Things to check for when scrubbing for PII. */ -export type CheckOnly = 'keys' | 'values' | 'both'; diff --git a/packages/fxa-shared/sentry/node.ts b/packages/fxa-shared/sentry/node.ts deleted file mode 100644 index bb667634967..00000000000 --- a/packages/fxa-shared/sentry/node.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* 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 * as Sentry from '@sentry/node'; -import { ErrorEvent } from '@sentry/core'; -import { SentryConfigOpts } from './models/SentryConfigOpts'; -import { ILogger } from '../log'; -import { tagCriticalEvent, tagFxaName } from './tag'; -import { buildSentryConfig } from './config-builder'; - -export type ExtraOpts = { - integrations?: any[]; - eventFilters?: Array<(event: ErrorEvent, hint: any) => ErrorEvent>; -}; - -export type InitSentryOpts = SentryConfigOpts & ExtraOpts; - -export function initSentry(config: InitSentryOpts, log: ILogger) { - if (!config?.sentry?.dsn) { - log.error( - 'sentry.dsn.missing', - 'No Sentry dsn provided. Cannot start sentry' - ); - return; - } - - const opts = buildSentryConfig(config, log); - const beforeSend = function (event: ErrorEvent, hint: any) { - // Default - event = tagCriticalEvent(event); - event = tagFxaName(event, config.sentry?.serverName || 'unknown'); - - // Custom filters - config.eventFilters?.forEach((filter) => { - event = filter(event, hint); - }); - return event; - }; - - const integrations = [ - // Default - Sentry.extraErrorDataIntegration({ depth: 5 }), - Sentry.requestDataIntegration(), - - // Custom Integrations - ...(config.integrations || []), - ]; - - try { - Sentry.init({ - // Defaults Options - normalizeDepth: 6, - maxValueLength: 500, - - // Custom Options - integrations, - beforeSend, - ...opts, - }); - } catch (e) { - log.debug('Issue initializing sentry!'); - log.error(e); - } -} diff --git a/packages/fxa-shared/sentry/pii-filter-actions.ts b/packages/fxa-shared/sentry/pii-filter-actions.ts deleted file mode 100644 index 5506f765bba..00000000000 --- a/packages/fxa-shared/sentry/pii-filter-actions.ts +++ /dev/null @@ -1,333 +0,0 @@ -/* 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 { CheckOnly, IFilterAction, PiiData } from './models/pii'; - -/** Default replacement values */ -export const FILTERED = '[Filtered]'; -export const TRUNCATED = '[Truncated]'; - -/** - * A filter that truncates anything over maxDepth. This is a good first action. - */ -export class DepthFilter implements IFilterAction { - /** - * Crete new depth filter. - * @param maxDepth - */ - constructor(protected readonly maxDepth = 3) {} - - execute(val: T, depth: number = 1) { - let exitPipeline = false; - - if (val == null) { - exitPipeline = true; - } else if (depth >= this.maxDepth && typeof val === 'object') { - Object.keys(val)?.forEach((x) => { - val[x] = TRUNCATED; - }); - exitPipeline = true; - } - - return { val, exitPipeline }; - } -} - -/** - * A filter truncates any object or array containing too many entries. - */ -export class BreadthFilter implements IFilterAction { - /** - * Create new breadth filter - * @param maxBreadth max number of values in object / array - */ - constructor(protected readonly maxBreadth: number) {} - - execute(val: T) { - let exitPipeline = false; - - if (val == null) { - exitPipeline = true; - } else if (typeof val === 'object') { - if (val instanceof Array) { - exitPipeline = this.maxBreadth == 0 || val.length === 0; - const deleted = val.splice(this.maxBreadth); - - // Leave some indication of what was deleted - if (deleted?.length) { - val.push(`${TRUNCATED}:${deleted.length}`); - } - } else { - const keys = Object.keys(val); - let count = 0; - for (const x of keys) { - if (++count > this.maxBreadth) { - delete val[x]; - } - } - - // Leave some indication of what was deleted - if (count > this.maxBreadth) { - val[TRUNCATED] = count - this.maxBreadth; - } - - exitPipeline = keys.length === 0 || this.maxBreadth === 0; - } - } - return { val, exitPipeline }; - } -} - -/** - * A base class for other PiiFilters. Supports checking keys and values - */ -export abstract class PiiFilter implements IFilterAction { - /** Flag determining if object values should be checked. */ - protected get checkValues() { - return this.checkOnly === 'values' || this.checkOnly === 'both'; - } - - /** Flag determining if object keys should be checked. */ - protected get checkKeys() { - return this.checkOnly === 'keys' || this.checkOnly === 'both'; - } - - /** - * Creates a new regex filter action - * @param checkOnly - Optional directive indicating what to check, a value, an object key, or both. - * @param replaceWith - Optional value indicating what to replace a matched value with. - */ - constructor( - public readonly checkOnly: CheckOnly = 'values', - public readonly replaceWith = FILTERED - ) {} - - /** - * Runs the filter - * @param val - value to filter on. - * @returns a filtered value - */ - public execute(val: T) { - let exitPipeline = false; - - if (val == null) { - exitPipeline = true; - } else if (typeof val === 'string') { - val = this.replaceValues(val) as T; - exitPipeline = val === this.replaceWith; - } else if (typeof val === 'object') { - exitPipeline = true; - - // Mutate object - for (const key of Object.keys(val)) { - if (this.filterKey(key)) { - val[key] = this.replaceWith; - } else if (this.filterValue(val[key])) { - val[key] = this.replaceValues(val[key]); - } - - // Encountering a non truncated or non filtered value means the pipeline must keep running. - if (exitPipeline && val[key] !== this.replaceWith) { - exitPipeline = false; - } - } - } - - return { val, exitPipeline }; - } - - /** - * Indicates if value should be filtered - * @param val - * @returns - */ - protected filterValue(val: any) { - return this.checkValues && typeof val === 'string'; - } - - /** - * Let the sub classes determine how to replace values. - * @param val - */ - protected abstract replaceValues(val: string): string; - - /** - * Let subclasses determine when an object's key should be filtered out. - * @param key - */ - protected abstract filterKey(key: string): boolean; -} - -/** - * Uses a regular expression to scrub PII - */ -export class PiiRegexFilter extends PiiFilter implements IFilterAction { - /** - * Creates a new regex filter action - * @param regex - regular expression to use for filter - * @param checkOnly - Optional directive indicating what to check, a value, an object key, or both. - * @param replaceWith - Optional value indicating what to replace a matched value with. - */ - constructor( - public readonly regex: RegExp, - public readonly checkOnly: CheckOnly = 'values', - public readonly replaceWith = FILTERED - ) { - super(checkOnly, replaceWith); - } - - protected override replaceValues(val: string): string { - return val.replace(this.regex, this.replaceWith); - } - - protected override filterKey(key: string): boolean { - const result = this.checkKeys && this.regex.test(key); - - // Tricky edge case. The regex maybe sticky. If so, we need to reset its lastIndex so it does not - // affect a subsequent operation. - if (this.regex.sticky) { - this.regex.lastIndex = 0; - } - return result; - } -} - -/** - * Makes sure that if value is a URL it doesn't have identifying info like the username or password portion of the url. - */ -export class UrlUsernamePasswordFilter extends PiiFilter { - constructor(replaceWith = FILTERED) { - super('values', replaceWith); - } - - protected override replaceValues(val: string) { - const url = tryParseUrl(val); - if (url) { - if (url.username) { - url.username = this.replaceWith; - } - if (url.password) { - url.password = this.replaceWith; - } - val = decodeURI(url.toString()); - } - return val; - } - - protected override filterKey(): boolean { - return false; - } -} - -/** - * Strips emails from data. - */ -export class EmailFilter extends PiiRegexFilter { - private readonly encode = [`'`, `"`, `=`]; - private readonly decode = [`[[[']]]`, `[[["]]]`, `[[[=]]]`]; - - constructor(checkOnly: CheckOnly = 'values', replaceWith = FILTERED) { - super( - // RFC 5322 generalized email regex, ~ 99.99% accurate. - /(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/gim, - checkOnly, - replaceWith - ); - } - - protected override replaceValues(val: string) { - const url = tryParseUrl(val); - if (url) { - if (url.search) { - url.search = url.search.replace(this.regex, this.replaceWith); - } - if (url.pathname) { - url.pathname = url.pathname.replace(this.regex, this.replaceWith); - } - try { - val = decodeURI(url.toString()); - } catch { - // Fallback incase the replaces made the url invalid - val = url.toString(); - } - } - - // Encode/decode to work around weird cases like email='foo@bar.com' which is - // technically a valid email, but ill advised and unlikely. Even if a user had - // this odd example email, the majority of the email would stripped, for example, - // email='[Filtered]' thereby eliminating PII. - this.encode.forEach((x, i) => { - val = val.replace(x, this.decode[i]); - }); - val = val.replace(this.regex, this.replaceWith); - this.decode.forEach((x, i) => { - val = val.replace(x, this.encode[i]); - }); - return val; - } - - protected filterKey(key: string): boolean { - return false; - } -} - -/** Auxillary method for safely parsing a url. If it can't be parsed returns null. */ -function tryParseUrl(val: string) { - try { - return new URL(val); - } catch (_) { - return null; - } -} - -/** - * Some common PII scrubbing actions - */ -export const CommonPiiActions = { - /** - * Limits object/arrays no more than 50 values. - */ - breadthFilter: new BreadthFilter(50), - - /** - * Limits objects to 5 levels of depth - */ - depthFilter: new DepthFilter(6), - - /** - * Makes sure the user name / password is stripped out of the url. - */ - urlUsernamePassword: new UrlUsernamePasswordFilter(), - - /** - * Makes sure emails are stripped from data. Uses RFC 5322 generalized email regex, ~ 99.99% accurate. - */ - emailValues: new EmailFilter(), - - /** - * Matches IP V6 values - */ - ipV6Values: new PiiRegexFilter( - /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gim - ), - - /** - * Matches IPV4 values - */ - ipV4Values: new PiiRegexFilter( - /(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/gim - ), - - /** - * Looks for keys that commonly contain PII - */ - piiKeys: new PiiRegexFilter( - /^oidc-.*|^remote-groups$|^uid$|^email_?|^ip_?|^user$|^user_?(id|name)$/i, - 'keys' - ), - - /** - * Matches uid, session, oauth and other common tokens which we would prefer not to include in Sentry reports. - */ - tokenValues: new PiiRegexFilter(/[a-fA-F0-9]{32,}|[a-fA-F0-9]{64,}/gim), -}; diff --git a/packages/fxa-shared/sentry/pii-filters.ts b/packages/fxa-shared/sentry/pii-filters.ts deleted file mode 100644 index 9facb930972..00000000000 --- a/packages/fxa-shared/sentry/pii-filters.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* 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 * as Sentry from '@sentry/node'; -import { ErrorEvent } from '@sentry/core'; - -import { Message } from '@aws-sdk/client-sqs'; - -import { ILogger } from '../log'; -import { IFilterAction, PiiData } from './models/pii'; - -/** - * Base class for all filters - */ -export abstract class FilterBase { - constructor( - protected readonly actions: IFilterAction[], - protected readonly logger?: ILogger - ) {} - - /** - * Applies filters to object and drills down into object - * @param val - Value to drill into - * @param depth - the current depth in the object - * @param maxDepth - depth at which to give up - * @returns - */ - applyFilters(val: T, depth = 1, maxDepth = 10): T { - if (depth < maxDepth) { - for (const x of this.actions) { - try { - const result = x.execute(val, depth); - val = result.val; - - // Exit pipeline early if value is not longer actionable. - if (result.exitPipeline) { - break; - } - } catch (err) { - this.logger?.error('sentry.filter.error', { err }); - } - } - - if (val != null && typeof val === 'object') { - Object.values(val).forEach((x) => { - this.applyFilters(x, depth + 1, maxDepth); - }); - } - } - - return val; - } - - abstract filter(data: PiiData): PiiData; -} - -/** - * Scrubs PII from SQS Messages - */ -export class SqsMessageFilter extends FilterBase { - /** - * Create a new SqsMessageFilter - * @param actions - */ - constructor(actions: IFilterAction[]) { - super(actions); - } - - /** - * Filter Body of sqs messages - */ - public filter(event: Message) { - this.filterBody(event); - return event; - } - - protected filterBody(event: Message) { - event.Body = this.applyFilters(event.Body); - return this; - } -} diff --git a/packages/fxa-shared/sentry/report-validation-error.ts b/packages/fxa-shared/sentry/report-validation-error.ts deleted file mode 100644 index a19f3ca6741..00000000000 --- a/packages/fxa-shared/sentry/report-validation-error.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { ValidationError } from 'joi'; - -/** - * Format a Stripe product/plan metadata validation error message for - * Sentry to include as much detail as possible about what metadata - * failed validation and in what way. - * - * @param {string} planId - * @param {string | ValidationError).ValidationError} error - */ -export function formatMetadataValidationErrorMessage( - planId: string, - error: ValidationError -) { - let msg = `${planId} metadata invalid:`; - if (typeof error === 'string') { - msg = `${msg} ${error}`; - } else { - msg = `${msg}${error.details - .map(({ message }) => ` ${message};`) - .join('')}`; - } - return msg; -} - -/** - * Report a validation error to Sentry with validation details. - * - * @param {Pick} sentry - Current sentry instance. Note, that this subtype is being - * used instead of directly accessing the sentry instance inorder to be context agnostic. - * @param {*} message - * @param {string | ValidationError} ValidationError error - */ -export function reportValidationError( - message: any, - error: ValidationError | string -) { - const details: any = {}; - if (typeof error === 'string') { - details.error = error; - } else { - for (const errorItem of error.details) { - const key = errorItem.path.join('.'); - details[key] = { - message: errorItem.message, - type: errorItem.type, - }; - } - } - - Sentry.withScope((scope) => { - scope.setContext('validationError', details); - Sentry.captureMessage(message, 'error'); - }); -} diff --git a/packages/fxa-shared/sentry/tag.ts b/packages/fxa-shared/sentry/tag.ts deleted file mode 100644 index 2ce663cb5bd..00000000000 --- a/packages/fxa-shared/sentry/tag.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* 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/. */ -const CRITICAL_ENDPOINTS = [ - // PORTS: 1111, 1112, 1113 - '/v1/avatar', - '/v1/profile', - '/v1/avatar/upload', - '/v1/display_name', - '/a/', - // PORTS: 3000, 3030 - '/', - '/settings', - '/settings/two_step_authentication/replace_codes', - '/sockjs-node', - '/authorization', - '/complete_reset_password', - '/metrics-flow', - '/settings', - '/settings/two_step_authentication/replace_codes', - '/signin', - '/metrics', - // PORTS: 4100 - '/', - // PORTS: 8080 - '/', - '/api/auth_status', - '/api/email_first', - '/api/oauth', - '/api/todos/get', - '/api/logout', - // PORTS: 9000, 9001 - '/v1/recoveryKey', - '/v1/account', - '/v1/account/attached_clients', - '/v1/account/keys', - '/v1/account/profile', - '/v1/client/', - '/v1/jwks', - '/v1/oauth/subscriptions/clients', - '/v1/password/forgot/status', - '/v1/recovery_email/status', - '/v1/recoveryKey/', - '/v1/totp/exists', - '/v1/account/attached_client/destroy', - '/v1/account/create', - '/v1/account/destroy', - '/v1/account/device', - '/v1/account/login', - '/v1/account/login/send_unblock_code', - '/v1/account/reset', - '/v1/account/status', - '/v1/oauth/authorization', - '/v1/oauth/token', - '/v1/password/change/finish', - '/v1/password/change/start', - '/v1/password/forgot/send_code', - '/v1/password/forgot/verify_code', - '/v1/recovery_email', - '/v1/recovery_email/destroy', - '/v1/recovery_email/secondary/verify_code', - '/v1/recovery_email/set_primary', - '/v1/recoveryKey', - '/v1/recoveryKey/exists', - '/v1/session/destroy', - '/v1/session/reauth', - '/v1/session/verify/recoveryCode', - '/v1/session/verify/totp', - '/v1/token', - '/v1/totp/create', - '/v1/totp/destroy', - '/v1/verify', - '/v1/recoveryCodes', - '/mail/', -]; - -/** - * Checks to see if the event is for a known critical endpoint. If so, - * tries to add a critical. - * @param data - */ -export function tagCriticalEvent(data: any) { - if (data && data.request && data.request.url) { - // It's possible value cannot be parsed. - let url: URL | undefined; - try { - url = new URL(data.request.url); - } catch (_err) {} - - if ( - url && - url.pathname && - CRITICAL_ENDPOINTS.some((x) => url?.pathname.startsWith(x)) - ) { - data.tags = data.tags || {}; - data.tags['fxa.endpoint.type'] = 'critical'; - } - } - - return data; -} - -export function tagFxaName(data: any, name?: string) { - data.tags = data.tags || {}; - data.tags['fxa.name'] = name || 'unknown'; - return data; -} diff --git a/packages/fxa-shared/test/sentry/browser.ts b/packages/fxa-shared/test/sentry/browser.ts deleted file mode 100644 index 1438ff3af89..00000000000 --- a/packages/fxa-shared/test/sentry/browser.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* 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 'jsdom-global/register'; -import { assert } from 'chai'; -import * as Sentry from '@sentry/browser'; -import sentryMetrics, { _Sentry } from '../../sentry/browser'; -import { SentryConfigOpts } from '../../sentry'; -import { ILogger } from '../../log'; -const sinon = require('sinon'); -const sandbox = sinon.createSandbox(); - -const config: SentryConfigOpts = { - release: 'v0.0.0', - sentry: { - dsn: 'https://public:private@host:8080/1', - env: 'test', - clientName: 'fxa-shared-testing', - sampleRate: 0, - }, -}; -const logger: ILogger = { - info(...args: any) {}, - trace(...args: any) {}, - warn(...args: any) {}, - error(...args: any) {}, - debug(...args: any) {}, -}; - -describe('sentry/browser', () => { - before(() => { - // Reduce console log noise in test output - sandbox.spy(console, 'error'); - }); - - beforeEach(() => { - // Make sure it's enabled by default - sentryMetrics.enable(); - }); - - after(() => { - sandbox.restore(); - }); - - describe('init', () => { - it('properly configures with dsn', () => { - sentryMetrics.configure(config, logger); - }); - }); - - describe('beforeSend', () => { - before(() => { - sentryMetrics.configure(config, logger); - }); - - it('works without request url', () => { - const data = { - key: 'value', - } as unknown as Sentry.ErrorEvent; - - const resultData = sentryMetrics.__beforeSend(config, data, {}); - - assert.equal(data, resultData); - }); - - it('fingerprints errno', () => { - const data = { - request: { - url: 'https://example.com', - }, - tags: { - errno: '100', - }, - } as unknown as Sentry.ErrorEvent; - - const resultData = sentryMetrics.__beforeSend(config, data, {}); - - assert.equal( - resultData!.fingerprint![0], - 'errno100', - 'correct fingerprint' - ); - assert.equal(resultData!.level, 'info', 'correct known error level'); - }); - }); - - describe('captureException', () => { - it('calls Sentry.captureException', () => { - const sentryCaptureException = sinon.stub(_Sentry, 'captureException'); - sentryMetrics.captureException(new Error('testo')); - sinon.assert.calledOnce(sentryCaptureException); - sentryCaptureException.restore(); - }); - }); - - describe('disable / enables', () => { - it('enables', () => { - sentryMetrics.enable(); - assert.isTrue(sentryMetrics.__sentryEnabled()); - }); - - it('disables', () => { - sentryMetrics.disable(); - assert.isFalse(sentryMetrics.__sentryEnabled()); - }); - - it('will return null from before send when disabled', () => { - sentryMetrics.disable(); - assert.isNull(sentryMetrics.__beforeSend({}, {} as any, {})); - }); - }); -}); diff --git a/packages/fxa-shared/test/sentry/config-builder.ts b/packages/fxa-shared/test/sentry/config-builder.ts deleted file mode 100644 index 1b9859a7a4c..00000000000 --- a/packages/fxa-shared/test/sentry/config-builder.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* 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 { assert } from 'chai'; -import { buildSentryConfig, SentryConfigOpts } from '../../sentry'; -import Sinon, { SinonSpiedInstance } from 'sinon'; -import { ILogger } from '../../log'; - -describe('config-builder', () => { - function cloneConfig(val: any) { - return JSON.parse(JSON.stringify(val)); - } - - const emptyLogger: ILogger = { - info: function (...args: any): void {}, - trace: function (...args: any): void {}, - warn: function (...args: any): void { - console.log('WARNING', ...args); - }, - error: function (...args: any): void {}, - debug: function (...args: any): void {}, - }; - - const sandbox = Sinon.createSandbox(); - const loggerSpy: SinonSpiedInstance = sandbox.spy(emptyLogger); - - afterEach(() => { - sandbox.reset(); - }); - - const testConfig: SentryConfigOpts = { - release: '1.0.1', - version: '1.0.2', - sentry: { - dsn: 'https://foo.sentry.io', - env: 'test', - sampleRate: 1, - serverName: 'fxa-shared-test', - clientName: 'fxa-shared-client-test', - }, - }; - - it('builds', () => { - const config = buildSentryConfig(testConfig, loggerSpy); - assert.exists(config); - assert.isTrue(loggerSpy.info.called); - }); - - it('picks correct defaults', () => { - const config = buildSentryConfig(testConfig, loggerSpy); - assert.equal(config.environment, testConfig.sentry?.env); - assert.equal(config.release, testConfig.release); - assert.equal(config.fxaName, testConfig.sentry?.clientName); - }); - - it('falls back', () => { - const clone = cloneConfig(testConfig); - delete clone.sentry.clientName; - delete clone.release; - - const config = buildSentryConfig(clone, loggerSpy); - - assert.equal(config.release, testConfig.version); - assert.equal(config.fxaName, testConfig.sentry?.serverName); - }); - - it('warns about missing config', () => { - const clone = cloneConfig(testConfig); - clone.sentry.dsn = ''; - - buildSentryConfig(clone, loggerSpy); - - assert.isTrue(loggerSpy.warn.called); - }); - - it('errors on missing dsn', () => { - const clone = JSON.parse(JSON.stringify(testConfig)); - clone.sentry.strict = true; - clone.sentry.dsn = ''; - - assert.throws(() => { - buildSentryConfig(clone, loggerSpy); - }, 'sentry.dsn not specified. sentry disabled.'); - assert.isTrue(loggerSpy.warn.called); - }); - - it('errors on unknown environment', () => { - const clone = cloneConfig(testConfig); - clone.sentry.strict = true; - clone.sentry.env = 'xyz'; - - assert.throws(() => { - buildSentryConfig(clone, loggerSpy); - }, 'invalid config.env. xyz options are: test,local,dev,ci,stage,prod,production,development'); - assert.isTrue(loggerSpy.warn.called); - }); - - it('errors on missing release', () => { - const clone = cloneConfig(testConfig); - clone.sentry.strict = true; - delete clone.release; - delete clone.version; - - assert.throws(() => { - buildSentryConfig(clone, loggerSpy); - }, 'config missing either release or version.'); - assert.isTrue(loggerSpy.warn.called); - }); - - it('errors on missing sampleRate', () => { - const clone = cloneConfig(testConfig); - clone.sentry.strict = true; - delete clone.sentry.sampleRate; - - assert.throws(() => { - buildSentryConfig(clone, loggerSpy); - }, 'sentry.sampleRate'); - assert.isTrue(loggerSpy.warn.called); - }); - - it('can use mozlogger', () => { - const mozlog = require('mozlog')({ - app: 'fxa-shared-test', - level: 'trace', - }); - const logger = mozlog('fxa-shared-testing'); - const config = buildSentryConfig(testConfig, logger); - - assert.exists(config); - }); - - it('can use console logger', () => { - const config = buildSentryConfig(testConfig, console); - assert.exists(config); - }); -}); diff --git a/packages/fxa-shared/test/sentry/event-tagging.ts b/packages/fxa-shared/test/sentry/event-tagging.ts deleted file mode 100644 index fc0f6d45e3b..00000000000 --- a/packages/fxa-shared/test/sentry/event-tagging.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* 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 { assert } from 'chai'; -import { Event } from '@sentry/core'; -import { tagCriticalEvent } from '../../sentry'; - -describe('critical-endpoints', () => { - it('adds critical tag to applicable event', () => { - let data: any = { - request: { - url: 'https://example.com/a/123', - }, - }; - - data = tagCriticalEvent(data); - - assert.exists(data.tags?.['fxa.endpoint.type']); - assert.equal(data.tags?.['fxa.endpoint.type'], 'critical'); - }); - - it('does not add critical tag to no applicable event', () => { - const data: Event = { - request: { - url: 'https://example.com/a-non-critical-endpoint', - }, - }; - - tagCriticalEvent(data); - - assert.notExists(data.tags?.['fxa.endpoint.importance']); - }); - - it('handles empty event', () => { - const data: Event = {}; - - tagCriticalEvent(data); - - assert.notExists(data.tags?.['fxa.endpoint.importance']); - }); - - it('handles empty url', () => { - const data: Event = { - request: { - url: undefined, - }, - }; - - tagCriticalEvent(data); - - assert.notExists(data.tags?.['fxa.endpoint.importance']); - }); -}); diff --git a/packages/fxa-shared/test/sentry/joi-message-overrides.ts b/packages/fxa-shared/test/sentry/joi-message-overrides.ts deleted file mode 100644 index 5f27a7e00fc..00000000000 --- a/packages/fxa-shared/test/sentry/joi-message-overrides.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* 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 { assert } from 'chai'; -import joi, { valid } from 'joi'; -import { overrideJoiMessages } from '../../sentry/joi-message-overrides'; - -describe('joi-message-overrides', () => { - it('overrides default message for regex', () => { - const validators = { - test: joi.string().regex(/test/), - }; - const result1 = validators.test.validate('foobar').error?.message; - - const validators2 = overrideJoiMessages(validators); - const result2 = validators2.test.validate('foobar').error?.message; - - assert.exists(validators2); - assert.exists(result1); - assert.exists(result2); - assert.notEqual(result1, result2); - assert.include(result1, 'with value'); - assert.notInclude(result2, 'with value'); - }); -}); diff --git a/packages/fxa-shared/test/sentry/pii-filter-actions.ts b/packages/fxa-shared/test/sentry/pii-filter-actions.ts deleted file mode 100644 index d036ecc5df2..00000000000 --- a/packages/fxa-shared/test/sentry/pii-filter-actions.ts +++ /dev/null @@ -1,381 +0,0 @@ -/* 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 { expect } from 'chai'; -import * as uuid from 'uuid'; - -import { - CommonPiiActions, - DepthFilter, - TRUNCATED, - FILTERED, - PiiRegexFilter, - BreadthFilter, -} from '../../sentry/pii-filter-actions'; - -describe('pii-filter-actions', () => { - describe('DepthFilter', () => { - it('truncates objects', () => { - const filter = new DepthFilter(1); - - expect(filter.execute('foo', 1)).to.deep.equal({ - val: 'foo', - exitPipeline: false, - }); - expect(filter.execute(null, 1)).to.deep.equal({ - val: null, - exitPipeline: true, - }); - }); - - it('truncates objects when depth is greater than or equal to max depth', () => { - const filter = new DepthFilter(1); - expect(filter.execute({ foo: 'bar' }, 1)).to.deep.equal({ - val: { - foo: TRUNCATED, - }, - exitPipeline: true, - }); - }); - - it('does not truncate if depth is less than max depth ', () => { - const filter = new DepthFilter(1); - expect(filter.execute({ foo: 'bar' }, 0)).to.deep.equal({ - val: { foo: 'bar' }, - exitPipeline: false, - }); - }); - - it('handles null', () => { - const filter = new DepthFilter(1); - expect(filter.execute(null, 1)).to.deep.equal({ - val: null, - exitPipeline: true, - }); - }); - }); - - describe('BreadthFilter', () => { - it('truncates objects', () => { - const filter = new BreadthFilter(1); - - expect(filter.execute('foo')).to.deep.equal({ - val: 'foo', - exitPipeline: false, - }); - expect(filter.execute(null)).to.deep.equal({ - val: null, - exitPipeline: true, - }); - }); - - it('truncates object of size greater than max breadth', () => { - const filter = new BreadthFilter(1); - expect(filter.execute({ foo: '1', bar: '2', baz: '3' })).to.deep.equal({ - val: { - foo: '1', - [TRUNCATED]: 2, - }, - exitPipeline: false, - }); - }); - - it('does not truncate object of size equal to max breadth', () => { - const filter = new BreadthFilter(3); - expect(filter.execute(['foo', 'bar', 'baz'])).to.deep.equal({ - val: ['foo', 'bar', 'baz'], - exitPipeline: false, - }); - }); - - it('does not truncate object of size less than max breadth', () => { - const filter = new BreadthFilter(5); - expect(filter.execute(['foo', 'bar', 'baz'])).to.deep.equal({ - val: ['foo', 'bar', 'baz'], - exitPipeline: false, - }); - }); - - it('truncates array of size greater than max breadth', () => { - const filter = new BreadthFilter(1); - expect(filter.execute(['foo', 'bar', 'baz'])).to.deep.equal({ - val: ['foo', `${TRUNCATED}:2`], - exitPipeline: false, - }); - }); - - it('does not truncate array of size less than max breadth', () => { - const filter = new BreadthFilter(5); - expect(filter.execute(['foo', 'bar', 'baz'])).to.deep.equal({ - val: ['foo', 'bar', 'baz'], - exitPipeline: false, - }); - }); - - it('does not truncate array of size equal to max breadth', () => { - const filter = new BreadthFilter(3); - expect(filter.execute(['foo', 'bar', 'baz'])).to.deep.equal({ - val: ['foo', 'bar', 'baz'], - exitPipeline: false, - }); - }); - - it('handles empty array', () => { - const filter = new BreadthFilter(1); - expect(filter.execute([])).to.deep.equal({ - val: [], - exitPipeline: true, - }); - }); - - it('handles empty object', () => { - const filter = new BreadthFilter(1); - expect(filter.execute({})).to.deep.equal({ - val: {}, - exitPipeline: true, - }); - }); - }); - - describe('PiiRegexFilter', () => { - it('filters string', () => { - const filter = new PiiRegexFilter(/foo/gi, 'values', '[BAR]'); - const value = filter.execute('test foo regex filter'); - expect(value).to.deep.equal({ - val: 'test [BAR] regex filter', - exitPipeline: false, - }); - }); - - it('filters string and determines exitPipeline', () => { - const filter = new PiiRegexFilter(/foo/gi, 'values', '[BAR]'); - const value = filter.execute('foo'); - expect(value).to.deep.equal({ val: '[BAR]', exitPipeline: true }); - }); - - it('filters object value', () => { - const filter1 = new PiiRegexFilter(/foo/gi, 'both', '[BAR]'); - const filter2 = new PiiRegexFilter(/foo/gi, 'values', '[BAR]'); - - const { val: value1 } = filter1.execute({ - item: 'test foo regex filter', - }); - const { val: value2 } = filter2.execute({ - item: 'test foo regex filter', - }); - - expect(value1.item).to.equal('test [BAR] regex filter'); - expect(value2.item).to.equal('test [BAR] regex filter'); - }); - - it('filters object key', () => { - const filter = new PiiRegexFilter(/foo/gi, 'keys', '[BAR]'); - - const { val: value } = filter.execute({ - foo: 'test foo regex filter', - }); - - expect(value.foo).to.equal('[BAR]'); - }); - - describe('checksOn', () => { - it('checks on values', () => { - const filter = new PiiRegexFilter(/foo/gi, 'values', '[BAR]'); - const { val: value } = filter.execute({ - foo: 'test foo regex filter', - bar: 'test foo regex filter', - }); - expect(value.foo).to.equal('test [BAR] regex filter'); - expect(value.bar).to.equal('test [BAR] regex filter'); - }); - - it('checks on keys', () => { - const filter = new PiiRegexFilter(/foo/gi, 'keys', '[BAR]'); - const { val: value } = filter.execute({ - foo: 'test foo regex filter', - bar: 'test foo regex filter', - }); - expect(value.foo).to.equal('[BAR]'); - expect(value.bar).to.equal('test foo regex filter'); - }); - - it('checks on keys and values', () => { - const filter = new PiiRegexFilter(/foo/gi, 'both', '[BAR]'); - const { val: value } = filter.execute({ - foo: 'test foo regex filter', - bar: 'test foo regex filter', - }); - expect(value.foo).to.equal('[BAR]'); - expect(value.bar).to.equal('test [BAR] regex filter'); - }); - }); - }); - - describe('CommonPiiActions', () => { - it('filters emails', () => { - const { val: result } = CommonPiiActions.emailValues.execute({ - foo: 'email: test@123.com -- 123@test.com --', - bar: '123', - }); - - expect(result).to.deep.equal({ - foo: `email: ${FILTERED} -- ${FILTERED} --`, - bar: '123', - }); - }); - - it('filters email in url', () => { - const { val: result } = CommonPiiActions.emailValues.execute( - 'http://foo.bar/?email=foxkey@mozilla.com&key=1' - ); - expect(result).to.equal(`http://foo.bar/?${FILTERED}&key=1`); - }); - - it('filters email in route', () => { - const { val: result } = CommonPiiActions.emailValues.execute( - '/account?email=foxkey@mozilla.com&key=1' - ); - expect(result).to.equal(`/account?email=${FILTERED}&key=1`); - }); - - it('filters email in query', () => { - const { val: result } = CommonPiiActions.emailValues.execute( - `where email='test@mozilla.com'` - ); - - expect(result).to.equal(`where email='${FILTERED}'`); - }); - - it('filters username / password from url', () => { - const { val: result } = CommonPiiActions.urlUsernamePassword.execute( - 'http://me:wut@foo.bar/' - ); - expect(result).to.equal(`http://${FILTERED}:${FILTERED}@foo.bar/`); - }); - - it('ipv6 values', () => { - const { val: result } = CommonPiiActions.ipV6Values.execute({ - foo: 'ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 -- FE80:0000:0000:0000:0202:B3FF:FE1E:8329 --', - bar: '123', - }); - expect(result).to.deep.equal({ - foo: `ipv6: ${FILTERED} -- ${FILTERED} --`, - bar: '123', - }); - }); - - it('ipv4 values', () => { - const { val: result } = CommonPiiActions.ipV4Values.execute({ - foo: '-- 127.0.0.1 -- 10.0.0.1 -- ', - bar: '1.2.3', - }); - expect(result).to.deep.equal({ - foo: `-- ${FILTERED} -- ${FILTERED} -- `, - bar: '1.2.3', - }); - }); - - it('filters pii keys', () => { - const { val: result } = CommonPiiActions.piiKeys.execute({ - 'oidc-test': 'foo', - 'OIDC-TEST': 'foo', - 'remote-groups': 'foo', - 'REMOTE-GROUPS': 'foo', - email_address: 'foo', - email: 'foo', - EmailAddress: 'foo', - ip: 'foo', - ip_addr: 'foo', - ip_address: 'foo', - IpAddress: 'foo', - uid: 'foo', - user: 'foo', - username: 'foo', - user_name: 'foo', - UserName: 'foo', - userid: 'foo', - UserId: 'foo', - user_id: 'foo', - bar: '123', - }); - - expect(result).to.deep.equal({ - 'oidc-test': FILTERED, - 'OIDC-TEST': FILTERED, - 'remote-groups': FILTERED, - 'REMOTE-GROUPS': FILTERED, - email: FILTERED, - email_address: FILTERED, - EmailAddress: FILTERED, - ip: FILTERED, - ip_addr: FILTERED, - ip_address: FILTERED, - IpAddress: FILTERED, - uid: FILTERED, - user: FILTERED, - username: FILTERED, - user_name: FILTERED, - UserName: FILTERED, - userid: FILTERED, - user_id: FILTERED, - UserId: FILTERED, - bar: '123', - }); - }); - - it('filters token values', () => { - const token1 = uuid.v4().replace(/-/g, ''); - const token2 = uuid.v4().replace(/-/g, ''); - const token3 = uuid.v4().toString(); - const { val: result } = CommonPiiActions.tokenValues.execute({ - foo: `-- ${token1}\n${token2}--`, - bar: token3, - }); - - expect(result).to.deep.equal({ - foo: `-- ${FILTERED}\n${FILTERED}--`, - bar: token3, - }); - }); - - it('filters 64 byte token values', () => { - const token1 = uuid.v4().replace(/-/g, ''); - const { val: result } = CommonPiiActions.tokenValues.execute({ - foo: `X'${token1}${token1}'`, - }); - - expect(result).to.deep.equal({ - foo: `X'${FILTERED}'`, - }); - }); - - it('filters token value in url', () => { - const result = CommonPiiActions.tokenValues.execute( - 'https://foo.bar/?uid=12345678123456781234567812345678' - ); - expect(result.val).to.equal(`https://foo.bar/?uid=${FILTERED}`); - }); - - it('filters token value in db statement', () => { - const result = CommonPiiActions.tokenValues.execute( - `Call accountDevices_17(X'cce22e4006d243c895c7596e2cad53d8',500)` - ); - expect(result.val).to.equal(`Call accountDevices_17(X'${FILTERED}',500)`); - }); - - it('filters token value in db query', () => { - const result = CommonPiiActions.tokenValues.execute( - ` where uid = X'cce22e4006d243c895c7596e2cad53d8' ` - ); - expect(result.val).to.equal(` where uid = X'${FILTERED}' `); - }); - - it('filters multiple multiline token values', () => { - const token = '12345678123456781234567812345678'; - const { val: result } = CommonPiiActions.tokenValues.execute( - `${token}--${token}\n${token}` - ); - expect(result).to.equal(`${FILTERED}--${FILTERED}\n${FILTERED}`); - }); - }); -}); diff --git a/packages/fxa-shared/test/sentry/pii-filters.ts b/packages/fxa-shared/test/sentry/pii-filters.ts deleted file mode 100644 index f293bc62a64..00000000000 --- a/packages/fxa-shared/test/sentry/pii-filters.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* 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 { ErrorEvent } from '@sentry/core'; -import { Message } from '@aws-sdk/client-sqs'; -import { expect } from 'chai'; -import sinon from 'sinon'; - -import { ILogger } from '../../log'; -import { IFilterAction, PiiData } from '../../sentry/models/pii'; -import { - CommonPiiActions, - FILTERED, - PiiRegexFilter, - TRUNCATED, -} from '../../sentry/pii-filter-actions'; -import { FilterBase, SqsMessageFilter } from '../../sentry/pii-filters'; - -describe('pii-filters', () => { - describe('SqsMessageFilter', () => { - const sqsFilter = new SqsMessageFilter([new PiiRegexFilter(/foo/gi)]); - - it('filters body', () => { - let msg = { Body: 'A message with foo in it.' } as Message; - msg = sqsFilter.filter(msg); - - expect(msg).to.deep.equal({ - Body: `A message with ${FILTERED} in it.`, - }); - }); - }); - - describe('Deals with Bad Filter', () => { - let mockLogger = { - error: () => {}, - }; - let sandbox = sinon.createSandbox(); - - afterEach(() => { - sandbox.restore(); - }); - - class BadAction implements IFilterAction { - execute( - val: T, - depth?: number - ): { val: T; exitPipeline: boolean } { - throw new Error('Boom'); - } - } - - class BadFilter extends FilterBase { - constructor(logger: ILogger) { - super([new BadAction()], logger); - } - - filter(data: any): any { - return this.applyFilters(data); - } - } - - it('handles errors and logs them', () => { - const errorStub = sandbox.stub(mockLogger, 'error'); - const badFilter = new BadFilter(mockLogger); - badFilter.filter({ foo: 'bar' }); - expect(errorStub.called).to.be.true; - }); - }); -}); diff --git a/packages/fxa-shared/test/tracing/config.ts b/packages/fxa-shared/test/tracing/config.ts deleted file mode 100644 index 45ed8332ead..00000000000 --- a/packages/fxa-shared/test/tracing/config.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* 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 { expect } from 'chai'; -import { - checkClientName, - checkSampleRate, - checkServiceName, -} from '../../tracing/config'; - -describe('tracing config', () => { - it('checks for client name', () => { - expect(() => checkClientName({ clientName: '' })).throws(); - }); - it('checks for service name', () => { - expect(() => checkServiceName({ serviceName: '' })).throws(); - }); - it('checks sample rate', () => { - expect(() => checkSampleRate({ sampleRate: -1 })).throws(); - expect(() => checkSampleRate({ sampleRate: 1.1 })).throws(); - expect(() => checkSampleRate({ sampleRate: Number.NaN })).throws(); - }); -}); diff --git a/packages/fxa-shared/test/tracing/exporters.ts b/packages/fxa-shared/test/tracing/exporters.ts deleted file mode 100644 index 33a9cdd6c48..00000000000 --- a/packages/fxa-shared/test/tracing/exporters.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* 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 { - BatchSpanProcessor, - ConsoleSpanExporter, - NodeTracerProvider, - SimpleSpanProcessor, -} from '@opentelemetry/sdk-trace-node'; -import { expect } from 'chai'; -import { getConsoleTraceExporter } from '../../tracing/exporters/fxa-console'; -import { getGcpTraceExporter } from '../../tracing/exporters/fxa-gcp'; -import { getOtlpTraceExporter } from '../../tracing/exporters/fxa-otlp'; -import sinon from 'sinon'; -import { TracingOpts } from '../../tracing/config'; -import { checkDuration } from '../../tracing/exporters/util'; - -describe('tracing exports', () => { - const sandbox = sinon.createSandbox(); - const provider = new NodeTracerProvider(); - - afterEach(() => { - sandbox.reset(); - }); - - describe('enables', () => { - const opts: TracingOpts = { - serviceName: 'test-service', - clientName: 'test-client', - sampleRate: 1, - corsUrls: '.*', - filterPii: true, - batchProcessor: false, - console: { - enabled: true, - }, - otel: { - enabled: true, - url: 'http://localhost:43180/v1/traces', - concurrencyLimit: 10, - }, - gcp: { - enabled: true, - }, - sentry: { - enabled: true, - }, - }; - - it('gets console exporter', () => { - expect(getConsoleTraceExporter(opts)).to.exist; - }); - - it('gets gcp exporter', () => { - expect(getGcpTraceExporter(opts)).to.exist; - }); - - it('gets otlp exporter', () => { - expect(getOtlpTraceExporter(opts)).to.exist; - }); - }); - - describe('disables', () => { - // Exporters not configured are effectively disabled - const opts: TracingOpts = { - serviceName: 'test-service', - clientName: 'test-client', - sampleRate: 1, - corsUrls: '.*', - batchProcessor: false, - filterPii: false, - }; - - it('does not get console exporter', () => { - expect(getConsoleTraceExporter(opts)).to.not.exist; - }); - - it('does not get gcp exporter', () => { - expect(getGcpTraceExporter(opts)).to.not.exist; - }); - - it('does not get otlp exporter', () => { - expect(getOtlpTraceExporter(opts)).to.not.exist; - }); - }); - - describe('util', () => { - it('prevents invalid durations', () => { - const span = { - startTime: [10, 0] as [number, number], - endTime: [0, 0] as [number, number], - duration: [-10, 0] as [number, number], - attributes: {} as Record, - }; - checkDuration(span); - - expect(span.duration[0]).to.equal(0); - expect(span.attributes['incorrect.duration']).to.equal('true'); - }); - }); -}); diff --git a/packages/fxa-shared/test/tracing/node-tracing.ts b/packages/fxa-shared/test/tracing/node-tracing.ts deleted file mode 100644 index d55caaf5165..00000000000 --- a/packages/fxa-shared/test/tracing/node-tracing.ts +++ /dev/null @@ -1,296 +0,0 @@ -/* 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 * as api from '@opentelemetry/api'; -import { Span } from '@opentelemetry/api'; -import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; -import { - BatchSpanProcessor, - NodeTracerProvider, - SimpleSpanProcessor, - SpanExporter, -} from '@opentelemetry/sdk-trace-node'; -import { expect } from 'chai'; -import proxyquire from 'proxyquire'; -import sinon from 'sinon'; -import { TracingOpts } from '../../tracing/config'; -import { ILogger } from '../../../../libs/shared/log/src'; - -proxyquire.noCallThru(); - -describe('node-tracing', () => { - const sandbox = sinon.createSandbox(); - const log_type = 'node-tracing'; - const contextManager = new AsyncHooksContextManager(); - - // Make sure there is an active context manager - api.context.setGlobalContextManager(contextManager); - - // Configure commonly used spies and mocks - const spies: any = { - register: sandbox.spy(NodeTracerProvider.prototype, 'register'), - logger: { - info: sandbox.spy(), - trace: sandbox.spy(), - warn: sandbox.spy(), - error: sandbox.spy(), - debug: sandbox.spy(), - }, - }; - const mocks: any = { - getGcpTraceExporter: sandbox.mock().callsFake(() => {}), - getConsoleExporter: sandbox.mock().callsFake(() => {}), - getOtlpTraceExporter: sandbox.mock().callsFake(() => {}), - }; - - // Proxy require exporters to prevent pulling in extra modules and to isolate tests. - const { - getCurrent, - getTraceParentId, - initTracing, - NodeTracingInitializer, - reset, - } = proxyquire('../../tracing/node-tracing', { - './exporters/fxa-console': { - getConsoleTraceExporter: mocks.getConsoleExporter, - }, - './exporters/fxa-gcp': { - getGcpTraceExporter: mocks.getGcpTraceExporter, - }, - './exporters/fxa-otlp': { - getOtlpTraceExporter: mocks.getOtlpTraceExporter, - }, - }); - - afterEach(() => { - sandbox.reset(); - }); - - after(() => { - sandbox.restore(); - }); - - it('requires a service name', () => { - expect(() => { - new NodeTracingInitializer( - { - serviceName: '', - sampleRate: 1, - }, - () => {}, - spies.logger - ); - }).to.throws('Missing config. serviceName must be defined!'); - }); - - it('initializes', () => { - new NodeTracingInitializer( - { - serviceName: 'test', - sampleRate: 1, - }, - spies.logger - ); - - sinon.assert.calledOnce(mocks.getConsoleExporter); - sinon.assert.calledOnce(mocks.getGcpTraceExporter); - sinon.assert.calledOnce(mocks.getOtlpTraceExporter); - }); - - it('starts span', async () => { - const tracing = new NodeTracingInitializer( - { - serviceName: 'test', - sampleRate: 1, - console: { - enabled: true, - }, - }, - spies.logger - ); - - tracing.startSpan('test', (span: Span) => { - expect(span.spanContext().traceId).to.exist; - }); - }); - - it('gets current trace id', () => { - const tracing = new NodeTracingInitializer( - { - serviceName: 'test', - sampleRate: 1, - }, - spies.logger - ); - - let traceId: string; - tracing.startSpan('test', (span: Span) => { - traceId = tracing.getTraceId(); - expect(span.spanContext().traceId).to.equal(traceId); - }); - }); - - it('gets parent trace id when not initialized', () => { - reset(); - expect(getTraceParentId()).to.equal('00-0-0-00'); - }); - - describe('parent trace id', () => { - it('gets parent trace id when initialized', () => { - initTracing( - { - serviceName: 'test', - sampleRate: 1, - console: { - enabled: true, - }, - }, - spies.logger - ); - expect(getTraceParentId()).to.exist; - expect(getTraceParentId()).to.not.equal('00-0-0-00'); - }); - - it('gets parent trace id when initialized', () => { - reset(); - expect(getTraceParentId()).to.exist; - expect(getTraceParentId()).to.equal('00-0-0-00'); - }); - }); - - describe('initTracing', () => { - afterEach(() => { - reset(); - }); - - function callInit() { - initTracing( - { - serviceName: 'test', - sampleRate: 1, - console: { - enabled: true, - }, - }, - spies.logger - ); - } - - it('skips initialization if all modes are disabled', () => { - initTracing( - { - serviceName: '', - sampleRate: 1, - }, - spies.logger - ); - sinon.assert.calledWithMatch(spies.logger.debug, log_type, { - msg: 'Trace initialization skipped. No exporters configured. Enable gcp, otel or console to activate tracing.', - }); - }); - - it('skips initialization if serviceName is missing', () => { - initTracing( - { - serviceName: '', - sampleRate: 1, - console: { - enabled: true, - }, - }, - spies.logger - ); - sinon.assert.calledWithMatch(spies.logger.error, log_type, { - msg: 'Trace initialization failed: Missing config. serviceName must be defined!', - }); - }); - - [null, -1, 2].forEach((sampleRate) => { - it('skips initialization if sampleRate is ' + sampleRate, () => { - initTracing( - { - serviceName: 'test', - sampleRate: sampleRate, - console: { - enabled: true, - }, - }, - spies.logger - ); - sinon.assert.calledWithMatch(spies.logger.error, log_type, { - msg: `Trace initialization failed: Invalid config. sampleRate must be a number between 0 and 1, but was ${sampleRate}.`, - }); - }); - }); - - it('initializes once', () => { - callInit(); - expect(getCurrent()).to.exist; - sinon.assert.calledWithMatch(spies.logger.info, log_type, { - msg: 'Trace initialized succeeded!', - }); - }); - - it('resets after initTracing', () => { - callInit(); - reset(); - expect(getCurrent()).to.not.exist; - }); - - it('warns of second initialization', () => { - callInit(); - callInit(); - sinon.assert.calledWithMatch(spies.logger.debug, log_type, { - msg: 'Trace initialization skipped. Tracing already initialized, ignoring new opts.', - }); - }); - }); - - /** - * Because `makeSpanProcessor` is private, we create a subclass - * to access it for testing. - */ - class TestableNodeTracingInitializer extends NodeTracingInitializer { - constructor(opts: TracingOpts, logger: ILogger) { - super(opts, logger); - } - public testMakeSpanProcessor(exporter: SpanExporter | undefined) { - return this.makeSpanProcessor(exporter); - } - } - - it('creates a BatchSpanProcessor when batchProcessor is true', () => { - const opts: TracingOpts = { - batchProcessor: true, - sampleRate: 1, - serviceName: 'test-service', - clientName: 'test-client', - corsUrls: '.*', - filterPii: true, - }; - const initializer = new TestableNodeTracingInitializer(opts, spies.logger); - - const exporter = { export: () => {}, shutdown: () => Promise.resolve() }; - const processor = initializer.testMakeSpanProcessor(exporter); - - expect(processor).to.be.instanceOf(BatchSpanProcessor); - }); - - it('creates a SimpleSpanProcessor when batchProcessor is false', () => { - const opts: TracingOpts = { - batchProcessor: false, - sampleRate: 1, - serviceName: 'test-service', - clientName: 'test-client', - corsUrls: '.*', - filterPii: true, - }; - const initializer = new TestableNodeTracingInitializer(opts, spies.logger); - - const exporter = { export: () => {}, shutdown: () => Promise.resolve() }; - const processor = initializer.testMakeSpanProcessor(exporter); - - expect(processor).to.be.instanceOf(SimpleSpanProcessor); - }); -}); diff --git a/packages/fxa-shared/test/tracing/pii-filter.ts b/packages/fxa-shared/test/tracing/pii-filter.ts deleted file mode 100644 index 5531dbfa454..00000000000 --- a/packages/fxa-shared/test/tracing/pii-filter.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* 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 { - Attributes, - HrTime, - Link, - SpanContext, - SpanKind, - SpanStatus, -} from '@opentelemetry/api'; -import { InstrumentationScope } from '@opentelemetry/core'; -import { Resource, resourceFromAttributes } from '@opentelemetry/resources'; -import { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; -import { expect } from 'chai'; -import sinon from 'sinon'; -import { FxaGcpTraceExporter } from '../../tracing/exporters/fxa-gcp'; -import { TracingPiiFilter } from '../../tracing/pii-filters'; - -describe('scrubs pii', () => { - const sandbox = sinon.createSandbox(); - - // Dummy ReadableSpan for testing - class FakeSpan implements ReadableSpan { - public readonly name: string = 'test'; - public readonly kind: SpanKind = SpanKind.INTERNAL; - public readonly spanContext: () => SpanContext = () => ({ - traceFlags: 0, - traceId: '123', - spanId: '123', - }); - public readonly parentSpanId?: string = '123'; - public readonly startTime: HrTime = [0, 0]; - public readonly endTime: HrTime = [0, 0]; - public readonly status: SpanStatus = { code: 0 }; - public readonly attributes: Attributes; - public readonly links: Link[] = []; - public readonly events: TimedEvent[] = []; - public readonly duration: HrTime = [0, 0]; - public readonly ended: boolean = true; - public readonly resource: Resource = resourceFromAttributes({}); - public readonly instrumentationLibrary: InstrumentationScope = { - name: '', - }; - - constructor(attributes: Attributes) { - this.attributes = attributes; - this.resource = resourceFromAttributes(attributes); - } - parentSpanContext?: SpanContext | undefined; - instrumentationScope: InstrumentationScope = { - name: '', - version: undefined, - }; - public readonly droppedAttributesCount: number = 0; - public readonly droppedEventsCount: number = 0; - public readonly droppedLinksCount: number = 0; - } - - afterEach(() => { - sandbox.reset(); - }); - - describe('pii filters', () => { - function check(key: string, val: string, mutation: string) { - const filter = new TracingPiiFilter(); - const exporter = new FxaGcpTraceExporter(filter); - const spans: ReadableSpan[] = [new FakeSpan({ [key]: val })]; - exporter.export(spans, () => {}); - expect(spans[0].attributes[key]).equals(mutation); - } - - it('filters pii from typical db.query', () => { - check( - 'db.query', - `select * from test where email = 'test@mozilla.com' or ip = '1.1.1.1' or uid = x'abcd1234abcd1234abcd1234abcd1234';`, - `select * from test where email = '[Filtered]' or ip = '[Filtered]' or uid = x'[Filtered]';` - ); - }); - - it('filters pii from typical db.statement call', () => { - check( - 'db.statement', - `Call deleteSessionToken_4(X'28d678b8d828ad79d6666877ae6c2919556a1bdaa1598efc264633203abbc279');`, - `Call deleteSessionToken_4(X'[Filtered]');` - ); - }); - - it('filters pii from typical http url call', () => { - check( - 'http.route', - `/v1/session/28d678b8d828ad79d6666877ae6c2919556a1bdaa1598efc264633203abbc279`, - `/v1/session/[Filtered]` - ); - }); - - it('filters pii from typical http url call', () => { - check( - 'http.route', - `/v1/find?email=test@mozilla.com`, - `/v1/find?email=[Filtered]` - ); - }); - - it('skips string payload', () => { - const filter = new TracingPiiFilter(); - const spy = sandbox.spy(filter, 'applyFilters'); - filter.filter('foo'); - - sinon.assert.notCalled(spy); - }); - - it('skips payload without attributes', () => { - const filter = new TracingPiiFilter(); - const spy = sandbox.spy(filter, 'applyFilters'); - filter.filter({ foo: 'bar' }); - - sinon.assert.notCalled(spy); - }); - - it('skips payload without relevant fields', () => { - const filter = new TracingPiiFilter(); - const spy = sandbox.spy(filter, 'applyFilters'); - filter.filter({ attributes: { foo: 'bar' } }); - - sinon.assert.notCalled(spy); - }); - - it('process payload with relevant fields', () => { - const filter = new TracingPiiFilter(); - const spy = sandbox.spy(filter, 'applyFilters'); - filter.filter({ attributes: { 'db.foo': 'bar' } }); - - sinon.assert.calledOnce(spy); - }); - }); -}); diff --git a/packages/fxa-shared/tracing/config.ts b/packages/fxa-shared/tracing/config.ts deleted file mode 100644 index a57e5ac2e0c..00000000000 --- a/packages/fxa-shared/tracing/config.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* 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/. */ - -/** - * Options for configuring tracing. - */ -export type TracingOpts = { - batchProcessor: boolean; - clientName: string; - corsUrls: string; - filterPii: boolean; - sampleRate: number; - serviceName: string; - - console?: { - enabled: boolean; - }; - gcp?: { - enabled: boolean; - }; - otel?: { - enabled: boolean; - url: string; - concurrencyLimit: number; - }; - sentry?: { - enabled: boolean; - }; -}; - -/** Default convict config for node tracing */ -export const tracingConfig = { - clientName: { - default: '', - doc: 'The name of client being traced.', - env: 'TRACING_CLIENT_NAME', - format: String, - }, - batchProcessor: { - default: true, - doc: 'Indicates if batch processing should be used. Batch processing is better for production environments.', - env: 'TRACING_BATCH_PROCESSING', - format: Boolean, - }, - corsUrls: { - default: 'http://localhost:\\d*/', - doc: 'A regex to allow tracing of cors requests', - env: 'TRACING_CORS_URLS', - format: String, - }, - filterPii: { - default: true, - doc: 'Enables filtering PII in Console traces.', - env: 'TRACING_FILTER_PII', - format: Boolean, - }, - sampleRate: { - default: 0, - doc: 'A number between 0 and 1 that indicates the rate at which to sample. 1 will capture all traces. .5 would capture half the traces, and 0 would capture no traces.', - env: 'TRACING_SAMPLE_RATE', - format: Number, - }, - serviceName: { - default: '', - doc: 'The name of service being traced.', - env: 'TRACING_SERVICE_NAME', - format: String, - }, - console: { - enabled: { - default: false, - doc: 'Trace report to the console', - env: 'TRACING_CONSOLE_EXPORTER_ENABLED', - format: Boolean, - }, - }, - gcp: { - enabled: { - default: false, - doc: 'Traces report to google cloud tracing. This should be turned on in the wild, but is discouraged for local development.', - env: 'TRACING_GCP_EXPORTER_ENABLED', - format: Boolean, - }, - }, - otel: { - enabled: { - default: false, - doc: 'Traces report to the otel. This is only applicable for local development.', - env: 'TRACING_OTEL_EXPORTER_ENABLED', - format: Boolean, - }, - url: { - default: 'http://localhost:4318/v1/traces', - doc: 'Open telemetry collector url', - env: 'TRACING_OTEL_URL', - format: String, - }, - concurrencyLimit: { - default: 10, - doc: 'Max amount of concurrency', - env: 'TRACING_OTEL_CONCURRENCY_LIMIT', - format: Number, - }, - }, - sentry: { - enabled: { - default: true, - doc: 'Allows traces to reported to sentry. Note that sentry must be initialized separately and first!.', - env: 'TRACING_SENTRY_EXPORTER_ENABLED', - format: Boolean, - }, - }, -}; - -export function checkServiceName(opts: Pick) { - if (!opts.serviceName) { - throw new Error('Missing config. serviceName must be defined!'); - } -} - -export function checkSampleRate(opts: Pick) { - if ( - opts.sampleRate == null || - Number.isNaN(opts.sampleRate) || - opts.sampleRate < 0 || - opts.sampleRate > 1 - ) { - throw new Error( - `Invalid config. sampleRate must be a number between 0 and 1, but was ${opts.sampleRate}.` - ); - } -} - -export function checkClientName(opts: Pick) { - if (!opts.clientName) { - throw new Error('Missing config. clientName must be defined!'); - } -} - -export function someModesEnabled( - opts: Pick -) { - return opts.otel?.enabled || opts.gcp?.enabled || opts.console?.enabled; -} diff --git a/packages/fxa-shared/tracing/exporters/fxa-console.ts b/packages/fxa-shared/tracing/exporters/fxa-console.ts deleted file mode 100644 index dfa9645107f..00000000000 --- a/packages/fxa-shared/tracing/exporters/fxa-console.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* 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 { ExportResult } from '@opentelemetry/core'; -import { - ConsoleSpanExporter, - ReadableSpan, -} from '@opentelemetry/sdk-trace-node'; -import { TracingOpts } from '../config'; -import { TracingPiiFilter } from '../pii-filters'; -import { checkDuration } from './util'; -import { ILogger } from '../../log'; - -/** Console Exporter exporter customized for FxA */ -export class FxaConsoleSpanExporter extends ConsoleSpanExporter { - constructor(protected readonly filter?: TracingPiiFilter) { - super(); - } - - override export( - spans: ReadableSpan[], - resultCallback: (result: ExportResult) => void - ) { - spans.forEach((x) => { - checkDuration(x); - this.filter?.filter(x); - }); - return super.export(spans, resultCallback); - } -} - -export function getConsoleTraceExporter( - opts: TracingOpts, - filter?: TracingPiiFilter, - logger?: ILogger -) { - if (!opts.console?.enabled) { - return; - } - logger?.debug('Adding Console Exporter'); - const exporter = new FxaConsoleSpanExporter(filter); - return exporter; -} diff --git a/packages/fxa-shared/tracing/exporters/fxa-gcp.ts b/packages/fxa-shared/tracing/exporters/fxa-gcp.ts deleted file mode 100644 index 8ec9706a9a2..00000000000 --- a/packages/fxa-shared/tracing/exporters/fxa-gcp.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* 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 { - TraceExporter as GcpTraceExporter, - TraceExporterOptions as GcpTraceExporterOptions, -} from '@google-cloud/opentelemetry-cloud-trace-exporter'; -import { ExportResult } from '@opentelemetry/core'; -import { ReadableSpan } from '@opentelemetry/sdk-trace-node'; -import { TracingOpts } from '../config'; -import { TracingPiiFilter } from '../pii-filters'; -import { checkDuration } from './util'; -import { ILogger } from '../../log'; -import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; - -/** Gcp exporter customized for FxA */ - -export class FxaGcpTraceExporter extends GcpTraceExporter { - constructor( - protected readonly filter?: TracingPiiFilter, - config?: GcpTraceExporterOptions - ) { - super(config); - } - - override export( - spans: ReadableSpan[], - resultCallback: (result: ExportResult) => void - ) { - spans.forEach((x) => { - checkDuration(x); - this.filter?.filter(x); - }); - return super.export(spans, resultCallback); - } -} - -export function getGcpTraceExporter( - opts: TracingOpts, - filter?: TracingPiiFilter, - logger?: ILogger -) { - if (!opts.gcp?.enabled) { - return; - } - - logger?.debug('Adding Gcp Trace Exporter'); - const exporter = new FxaGcpTraceExporter(filter); - return exporter; -} diff --git a/packages/fxa-shared/tracing/exporters/fxa-otlp.ts b/packages/fxa-shared/tracing/exporters/fxa-otlp.ts deleted file mode 100644 index 42a31202106..00000000000 --- a/packages/fxa-shared/tracing/exporters/fxa-otlp.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* 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 { ExportResult } from '@opentelemetry/core'; -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; -import { OTLPExporterConfigBase } from '@opentelemetry/otlp-exporter-base'; -import { ReadableSpan } from '@opentelemetry/sdk-trace-node'; -import { TracingOpts } from '../config'; -import { TracingPiiFilter } from '../pii-filters'; -import { checkDuration } from './util'; -import { ILogger } from '../../log'; - -export type FxaOtlpTracingHeaders = { - flowid?: string; - traceparent?: string; - tracestate?: string; -}; - -/** OTLP exporter customized for FxA */ -export class FxaOtlpWebExporter extends OTLPTraceExporter { - constructor( - protected readonly filter?: TracingPiiFilter, - config?: OTLPExporterConfigBase, - protected readonly logger?: ILogger - ) { - super(config); - } - - override export( - spans: ReadableSpan[], - resultCallback: (result: ExportResult) => void - ) { - spans.forEach((x) => { - checkDuration(x); - this.filter?.filter(x); - }); - super.export(spans, (result) => { - if (result.error) { - this.logger?.error(result.error); - } - resultCallback(result); - }); - } -} - -export function getOtlpTraceExporter( - opts: TracingOpts, - headers?: FxaOtlpTracingHeaders, - filter?: TracingPiiFilter, - logger?: ILogger -) { - if (!opts.otel?.enabled) { - return; - } - - logger?.debug('Adding Otlp Trace Exporter ', opts.otel?.url); - const config = { - url: opts.otel?.url, - headers, - concurrencyLimit: opts.otel?.concurrencyLimit, - }; - const exporter = new FxaOtlpWebExporter(filter, config, logger); - return exporter; -} diff --git a/packages/fxa-shared/tracing/exporters/util.ts b/packages/fxa-shared/tracing/exporters/util.ts deleted file mode 100644 index cd077d2da2f..00000000000 --- a/packages/fxa-shared/tracing/exporters/util.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* 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 { ReadableSpan } from '@opentelemetry/sdk-trace-node'; - -/** - * Checks, fixes, and flags bad duration state on spans. - * @param span - */ -export function checkDuration( - span: Pick -) { - // Temporary Hack: There appears to be bug in the client side instrumentation code - // that results in negative durations. These negative durations end up being - // coerced into unsigned ints creating very odd and usually large values, which - // obscures the entire trace's timing. To work around this, we will zero out these - // durations and set an incorrect.duration flag. - // - // Note, that duration is of type HrTime. See Type definition to understand its - // format. - // - - let adjusted = false; - // Adjust epoch - if (span.startTime[0] > span.endTime[0]) { - span.startTime[0] = span.endTime[0]; - adjusted = true; - } - - // Adjust nano seconds - if ( - span.startTime[0] === span.endTime[0] && - span.startTime[1] > span.endTime[1] - ) { - span.endTime[1] = span.startTime[1]; - adjusted = true; - } - - if (adjusted) { - span.duration[0] = span.endTime[0] - span.startTime[0]; - span.duration[1] = span.endTime[1] - span.startTime[1]; - span.attributes['incorrect.duration'] = 'true'; - } -} diff --git a/packages/fxa-shared/tracing/node-tracing.ts b/packages/fxa-shared/tracing/node-tracing.ts deleted file mode 100644 index 4bcc7b8ba89..00000000000 --- a/packages/fxa-shared/tracing/node-tracing.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* 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 api from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; -import { ILogger } from '../log'; -import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; -import { registerInstrumentations } from '@opentelemetry/instrumentation'; -import { - BatchSpanProcessor, - NodeTracerProvider, - ParentBasedSampler, - SimpleSpanProcessor, - SpanExporter, - SpanProcessor, - TraceIdRatioBasedSampler, -} from '@opentelemetry/sdk-trace-node'; -import { checkSampleRate, checkServiceName, TracingOpts } from './config'; -import { getConsoleTraceExporter } from './exporters/fxa-console'; -import { getGcpTraceExporter } from './exporters/fxa-gcp'; -import { getOtlpTraceExporter } from './exporters/fxa-otlp'; -import { createPiiFilter } from './pii-filters'; -import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; -import { resourceFromAttributes } from '@opentelemetry/resources'; -const log_type = 'node-tracing'; - -export const TRACER_NAME = 'fxa'; - -/** - * Responsible for initializing node tracing from a config object. This uses the auto instrumentation feature - * which tries to add as much instrumentation as possible. See the 'supported instrumentations section at - * https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node for more info. - */ -export class NodeTracingInitializer { - protected provider: NodeTracerProvider; - - constructor( - protected readonly opts: TracingOpts, - protected readonly logger?: ILogger - ) { - // Error out if certain options are invalid - checkServiceName(this.opts); - checkSampleRate(this.opts); - - const filter = createPiiFilter(!!this.opts?.filterPii, this.logger); - - const spanProcessors = [ - this.makeSpanProcessor( - getOtlpTraceExporter(this.opts, undefined, filter) - ), - this.makeSpanProcessor(getGcpTraceExporter(this.opts, filter)), - this.makeSpanProcessor(getConsoleTraceExporter(this.opts, filter)), - // add more exporters here - ].filter((x) => x !== undefined); - - this.provider = new NodeTracerProvider({ - sampler: new ParentBasedSampler({ - root: new TraceIdRatioBasedSampler(this.opts.sampleRate), - }), - resource: resourceFromAttributes({ - [ATTR_SERVICE_NAME]: this.opts.serviceName, - }), - spanProcessors, - }); - - this.register(); - } - - /** - * Creates a new span processor for the exporter. - * @param exporter - * @returns - */ - private makeSpanProcessor = ( - exporter: SpanExporter | undefined - ): SpanProcessor | undefined => { - if (!exporter) { - return undefined; - } - return this.opts.batchProcessor - ? new BatchSpanProcessor(exporter) - : new SimpleSpanProcessor(exporter); - }; - - protected register() { - registerInstrumentations({ - instrumentations: [ - // ...extraInstrumentations, - getNodeAutoInstrumentations({ - // These instrumentations added a lot of unnecessary noise - '@opentelemetry/instrumentation-dns': { - enabled: false, - }, - '@opentelemetry/instrumentation-net': { - enabled: false, - }, - '@opentelemetry/instrumentation-fs': { - enabled: false, - }, - }), - ], - }); - this.provider.register(); - } - - public startSpan(name: string, action: () => void) { - return this.provider.getTracer(TRACER_NAME).startActiveSpan(name, action); - } - - /** Gets current traceId */ - public getTraceId() { - const currentSpan = api.trace.getSpan(api.context.active()); - if (currentSpan) { - return currentSpan.spanContext().traceId; - } - return null; - } - - public getTraceParentId() { - const tracer = this.provider.getTracer('fxa'); - const span = tracer.startSpan('client-inject'); - const version = '00'; - const spanContext = span.spanContext(); - let sampleDecision = '00'; - if (Math.random() <= this.opts.sampleRate) { - sampleDecision = '01'; - } - const parentId = `${version}-${spanContext.traceId}-${spanContext.spanId}-${sampleDecision}`; - span.end(); - return parentId; - } - - public getProvider() { - return this.provider; - } -} - -/** Singleton */ -let nodeTracing: NodeTracingInitializer | undefined; - -/** Gets active trace parent id */ -export function getTraceParentId() { - if (nodeTracing == null) { - return '00-0-0-00'; - } - return nodeTracing.getTraceParentId(); -} - -/** Initializes tracing in node context */ -export function initTracing(opts: TracingOpts, logger: ILogger) { - if (nodeTracing != null) { - logger?.debug(log_type, { - msg: 'Trace initialization skipped. Tracing already initialized, ignoring new opts.', - }); - return nodeTracing; - } - - if ( - !opts.otel?.enabled && - !opts.gcp?.enabled && - !opts.console?.enabled && - !opts.sentry?.enabled - ) { - logger.debug(log_type, { - msg: 'Trace initialization skipped. No exporters configured. Enable gcp, otel or console to activate tracing.', - }); - return; - } - - try { - nodeTracing = new NodeTracingInitializer(opts, logger); - logger.info(log_type, { msg: 'Trace initialized succeeded!' }); - } catch (err) { - logger.error(log_type, { - msg: `Trace initialization failed: ${err.message}`, - }); - } - return nodeTracing; -} - -/** Get the current instance of the node tracing provider. */ -export function getCurrent() { - return nodeTracing; -} - -/** Indicates that tracing has been initialized. */ -export function isInitialized() { - return !!nodeTracing; -} - -/** Suppresses trace capture on the current context */ -export function suppressTrace(action: () => any) { - const currentCtx = api.context.active(); - return api.context.with(suppressTracing(currentCtx), action); -} - -/** Resets the current tracing instance. Use only for testing purposes. */ -export function reset() { - nodeTracing = undefined; -} diff --git a/packages/fxa-shared/tracing/pii-filters.ts b/packages/fxa-shared/tracing/pii-filters.ts deleted file mode 100644 index 73752451c3c..00000000000 --- a/packages/fxa-shared/tracing/pii-filters.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* 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 { ILogger } from '../log'; -import { PiiData } from '../sentry/models/pii'; -import { CommonPiiActions } from '../sentry/pii-filter-actions'; -import { FilterBase } from '../sentry/pii-filters'; - -/** Matches attribute names that need to be filtered. */ -const reTargetPiiAttributes = /^(db|http)\./; - -/** - * PiiFilter specifically for scrubbing open telemetry traces. - */ -export class TracingPiiFilter extends FilterBase { - /** - * Creates new PII Filter for tracing - * @param logger - optional logger - */ - constructor(logger?: ILogger) { - super( - [ - CommonPiiActions.breadthFilter, - CommonPiiActions.depthFilter, - CommonPiiActions.piiKeys, - CommonPiiActions.emailValues, - CommonPiiActions.tokenValues, - CommonPiiActions.ipV4Values, - CommonPiiActions.ipV6Values, - CommonPiiActions.urlUsernamePassword, - ], - logger - ); - } - - /** - * Filter traces. - * @param data - Data to filter. - */ - filter(data: PiiData): PiiData { - try { - if (typeof data === 'object' && data?.attributes) { - for (const key of Object.keys(data.attributes)) { - if (reTargetPiiAttributes.test(key)) { - data.attributes[key] = this.applyFilters(data.attributes[key]); - } - } - } - } catch (err) { - // Note, we have to throw the error, since the trace could contain PII if - // the routine doesn't exist cleanly. We will log this, so there's some way - // to track the problem down if we notice missing spans. - this.logger?.error('pii-trace-filter', err); - throw err; - } - - return data; - } -} - -/** Singleton */ -let piiFilter: TracingPiiFilter; - -/** Creates a PII filter for tracing. This behaves as a singleton. */ -export function createPiiFilter(enabled: boolean, logger?: ILogger) { - if (!enabled) { - return; - } - - if (!piiFilter) { - piiFilter = new TracingPiiFilter(logger); - } - - return piiFilter; -} diff --git a/tsconfig.base.json b/tsconfig.base.json index d56fc4fe526..8d4decccd1a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -90,6 +90,7 @@ "@fxa/shared/log": ["libs/shared/log/src/index.ts"], "@fxa/shared/metrics/glean": ["libs/shared/metrics/glean/src/index.ts"], "@fxa/shared/metrics/statsd": ["libs/shared/metrics/statsd/src/index.ts"], + "@fxa/shared/monitoring": ["libs/shared/monitoring/src/index.ts"], "@fxa/shared/mozlog": ["libs/shared/mozlog/src/index.ts"], "@fxa/shared/nestjs/customs": ["libs/shared/nestjs/customs/src/index.ts"], "@fxa/shared/notifier": ["libs/shared/notifier/src/index.ts"],