feat(next): add sentry to payments-next

Because:

- Be able to use Sentry in Next.js on both the client and the server

This commit:

- Add Sentry to payments-next using the installation wizard

Closes #FXA-9998
This commit is contained in:
Reino Muhl 2024-09-05 14:55:11 -04:00
Родитель 3ac75baec1
Коммит 54fefd2872
Не найден ключ, соответствующий данной подписи
24 изменённых файлов: 1473 добавлений и 10 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -177,3 +177,6 @@ tmp
# shared-cms
libs/shared/cms/src/__generated__/graphql.d.ts
libs/shared/cms/src/__generated__/graphql.js
# Sentry Config File
.env.sentry-build-plugin

Просмотреть файл

@ -79,5 +79,18 @@ STATS_D_CONFIG__PREFIX=
CSP__ACCOUNTS_STATIC_CDN=https://accounts-static.cdn.mozilla.net
CSP__PAYPAL_API='https://www.sandbox.paypal.com'
# Sentry Config
SENTRY__SERVER_NAME=fxa-payments-next-server
SENTRY__AUTH_TOKEN=
# Other
CONTENT_SERVER_URL=http://localhost:3030
# Nextjs Public Environment Variables
# Sentry Config
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_ENV=local
NEXT_PUBLIC_SENTRY_CLIENT_NAME=fxa-payments-next-client
NEXT_PUBLIC_SENTRY_SAMPLE_RATE=1
NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=1

Просмотреть файл

@ -75,5 +75,18 @@ STATS_D_CONFIG__PREFIX=
CSP__ACCOUNTS_STATIC_CDN=https://accounts-static.cdn.mozilla.net
CSP__PAYPAL_API='https://www.paypal.com'
# Sentry Config
SENTRY__SERVER_NAME=fxa-payments-next-server
SENTRY__AUTH_TOKEN=
# Other
CONTENT_SERVER_URL=https://accounts.firefox.com
# Nextjs Public Environment Variables
# Sentry Config
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_ENV=prod
NEXT_PUBLIC_SENTRY_CLIENT_NAME=fxa-payments-next-client
NEXT_PUBLIC_SENTRY_SAMPLE_RATE=1
NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=1

Просмотреть файл

@ -0,0 +1,31 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use client';
import * as Sentry from '@sentry/nextjs';
import NextError from 'next/error';
import { useEffect } from 'react';
export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
<NextError statusCode={0} />
</body>
</html>
);
}

Просмотреть файл

@ -1,7 +1,18 @@
/* 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 'reflect-metadata';
import 'server-only';
import { Type } from 'class-transformer';
import { IsString, ValidateNested, IsDefined, IsUrl } from 'class-validator';
import {
IsString,
ValidateNested,
IsDefined,
IsUrl,
IsNumber,
IsOptional,
} from 'class-validator';
import {
RootConfig as NestAppRootConfig,
validate,
@ -20,6 +31,14 @@ class PaypalConfig {
clientId!: string;
}
class SentryServerConfig {
@IsString()
serverName!: string;
@IsString()
authToken!: string;
}
class AuthJSConfig {
@IsUrl({ require_tld: false })
issuerUrl!: string;
@ -47,6 +66,11 @@ export class PaymentsNextConfig extends NestAppRootConfig {
@IsDefined()
csp!: CspConfig;
@Type(() => SentryServerConfig)
@ValidateNested()
@IsDefined()
sentry!: SentryServerConfig;
@IsString()
authSecret!: string;
@ -55,6 +79,29 @@ export class PaymentsNextConfig extends NestAppRootConfig {
@IsUrl({ require_tld: false })
contentServerUrl!: string;
/**
* Nextjs Public Environment Variables
*/
/**
* Sentry Config
*/
@IsOptional()
@IsString()
nextPublicSentryDsn?: string;
@IsString()
nextPublicSentryEnv!: string;
@IsString()
nextPublicSentryClientName!: string;
@IsNumber()
nextPublicSentrySampleRate!: number;
@IsNumber()
nextPublicSentryTracesSampleRate!: number;
}
export const config = validate(process.env, PaymentsNextConfig);

Просмотреть файл

@ -1,5 +1,10 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
const { getApp } = await import('@fxa/payments/ui/server');
await getApp().initialize();

Просмотреть файл

@ -6,6 +6,7 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { composePlugins, withNx } = require('@nx/next');
const { withSentryConfig } = require('@sentry/nextjs');
/**
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
@ -22,6 +23,8 @@ const nextConfig = {
'@nestjs/core',
'@nestjs/common',
'@nestjs/websockets',
'@nestjs/graphql',
'@nestjs/mapped-types',
'class-transformer',
'class-validator',
'hot-shots',
@ -50,9 +53,54 @@ const nextConfig = {
},
};
/**
* @type {import('@sentry/nextjs').SentryBuildOptions}
**/
const sentryOptions = {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "mozilla",
project: "fxa-payments-next",
// Enable source maps
authToken: process.env.SENTRY_AUTH_TOKEN,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Automatically annotate React components to show their full name in breadcrumbs and session replay
reactComponentAnnotation: {
enabled: true,
},
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
}
// Use withSentryConfig to wrap the next config
const sentryEnhancedConfig = (passedConfig) =>
withSentryConfig(passedConfig, sentryOptions);
const plugins = [
// Add more Next.js plugins to this list if needed.
withNx,
sentryEnhancedConfig,
];
module.exports = composePlugins(...plugins)(nextConfig);

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "payments-next",
"version": "0.0.1",
"version": "0.0.0",
"scripts": {
"start": "next start"
}

Просмотреть файл

@ -0,0 +1,30 @@
/* 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/. */
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import { initSentryForNextjsClient } from '@fxa/shared/sentry/client';
import { version } from './package.json';
const DEFAULT_SAMPLE_RATE = '1';
const DEFAULT_TRACES_SAMPLE_RATE = '1';
const sentryConfig = {
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
env: process.env.NEXT_PUBLIC_SENTRY_ENV,
clientName: process.env.NEXT_PUBLIC_SENTRY_CLIENT_NAME,
sampleRate: parseInt(
process.env.NEXT_PUBLIC_SENTRY_SAMPLE_RATE || DEFAULT_SAMPLE_RATE
),
tracesSampleRate: parseInt(
process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE ||
DEFAULT_TRACES_SAMPLE_RATE
),
};
initSentryForNextjsClient({
release: version,
sentry: sentryConfig,
});

Просмотреть файл

@ -0,0 +1,26 @@
/* 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/. */
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import { initSentryForNextjsServer } from '@fxa/shared/sentry';
import { config } from './config';
import { version } from './package.json';
const sentryConfig = {
dsn: config.nextPublicSentryDsn,
env: config.nextPublicSentryEnv,
serverName: config.sentry.serverName,
sampleRate: config.nextPublicSentrySampleRate,
tracesSampleRate: config.nextPublicSentryTracesSampleRate,
};
initSentryForNextjsServer(
{
release: version,
sentry: sentryConfig,
},
console
);

Просмотреть файл

@ -1,3 +1,7 @@
/* 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 set from 'set-value';
import { plainToInstance, ClassConstructor } from 'class-transformer';
import { validateSync } from 'class-validator';

Просмотреть файл

@ -0,0 +1,5 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export * from './lib/next/client';

Просмотреть файл

@ -8,3 +8,4 @@ export * from './lib/nest/sentry.constants';
export * from './lib/reporting';
export * from './lib/node';
export * from './lib/browser';
export * from './lib/next/server';

Просмотреть файл

@ -0,0 +1,54 @@
/* 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/. */
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs';
import { SentryConfigOpts } from '../models/SentryConfigOpts';
import { buildSentryConfig } from '../config-builder';
import { Logger } from '../sentry.types';
import { beforeSend } from '../utils/beforeSend.client';
/**
* @@todo - To be worked on in FXA-10398
*/
const sentryEnabled = true;
export function initSentryForNextjsClient(
config: SentryConfigOpts,
log?: Logger
) {
if (!log) {
log = console;
}
if (!config?.sentry?.dsn) {
log.error('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);
try {
Sentry.init({
...opts,
integrations: [
Sentry.browserTracingIntegration({
enableInp: true,
}),
],
beforeSend: function (event: Sentry.ErrorEvent) {
return beforeSend(sentryEnabled, opts, event);
},
});
} catch (e) {
log.error(e);
}
}

Просмотреть файл

@ -0,0 +1,63 @@
/* 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/nextjs';
import { ErrorEvent } from '@sentry/types';
import { SentryConfigOpts } from '../models/SentryConfigOpts';
import { buildSentryConfig } from '../config-builder';
import { Logger } from '../sentry.types';
import { tagFxaName } from '../utils/tagFxaName';
type ExtraOpts = {
integrations?: any[];
eventFilters?: Array<(event: ErrorEvent, hint: any) => ErrorEvent>;
};
type InitSentryOpts = SentryConfigOpts & ExtraOpts;
export function initSentryForNextjsServer(config: InitSentryOpts, log: Logger) {
if (!config?.sentry?.dsn) {
log.error('No Sentry dsn provided. Cannot start sentry');
return;
}
const opts = buildSentryConfig(config, log);
/**
* @@todo - Move to lib/utils/beforeSend.server.ts - FXA-10402
*/
const beforeSend = function (event: ErrorEvent, hint: any) {
// Default
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 }),
// Custom Integrations
...(config.integrations || []),
];
try {
Sentry.init({
// Defaults Options
normalizeDepth: 6,
maxValueLength: 500,
// Custom Options
integrations,
beforeSend,
...opts,
});
} catch (e) {
log.debug('init-sentry', { msg: 'Issue initializing sentry!' });
log.error('init-sentry', e);
}
}

Просмотреть файл

@ -24,6 +24,9 @@ export function initSentry(config: InitSentryOpts, log: Logger) {
}
const opts = buildSentryConfig(config, log);
/**
* @@todo - Move to lib/utils/beforeSend.server.ts - FXA-10402
*/
const beforeSend = function (event: ErrorEvent, hint: any) {
// Default
event = tagFxaName(event, config.sentry?.serverName || 'unknown');

Просмотреть файл

@ -0,0 +1,137 @@
/* 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 { SentryConfigOpts } from '../models/SentryConfigOpts';
import { beforeSend } from './beforeSend.client';
import * as Sentry from '@sentry/nextjs';
const config: SentryConfigOpts = {
release: 'v0.0.0',
sentry: {
dsn: 'https://public:private@host:8080/1',
env: 'test',
clientName: 'fxa-shared-testing',
sampleRate: 0,
},
};
const sentryEnabled = true;
describe('beforeSend', () => {
it('works without request url', () => {
const data = {
key: 'value',
} as unknown as Sentry.ErrorEvent;
const resultData = beforeSend(sentryEnabled, config, data);
expect(data).toEqual(resultData);
});
it('fingerprints errno', () => {
const data = {
request: {
url: 'https://example.com',
},
tags: {
errno: '100',
},
type: undefined,
} as Sentry.ErrorEvent;
const resultData = beforeSend(sentryEnabled, config, data);
expect(resultData?.fingerprint?.[0]).toEqual('errno100');
expect(resultData?.level).toEqual('info');
});
it('properly erases sensitive information from url', () => {
const url = 'https://accounts.firefox.com/complete_reset_password';
const badQuery =
'?token=foo&code=bar&email=some%40restmail.net&service=sync';
const goodQuery = '?token=VALUE&code=VALUE&email=VALUE&service=sync';
const badData = {
request: {
url: url + badQuery,
},
} as Sentry.ErrorEvent;
const goodData = {
request: {
url: url + goodQuery,
},
};
const resultData = beforeSend(sentryEnabled, config, badData);
expect(resultData?.request?.url).toEqual(goodData.request.url);
});
it('properly erases sensitive information from referrer', () => {
const url = 'https://accounts.firefox.com/complete_reset_password';
const badQuery =
'?token=foo&code=bar&email=some%40restmail.net&service=sync';
const goodQuery = '?token=VALUE&code=VALUE&email=VALUE&service=sync';
const badData = {
request: {
headers: {
Referer: url + badQuery,
},
},
type: undefined,
} as Sentry.ErrorEvent;
const goodData = {
request: {
headers: {
Referer: url + goodQuery,
},
},
};
const resultData = beforeSend(sentryEnabled, config, badData);
expect(resultData?.request?.headers?.Referer).toEqual(
goodData.request.headers.Referer
);
});
it('properly erases sensitive information from abs_path', () => {
const url = 'https://accounts.firefox.com/complete_reset_password';
const badCulprit = 'https://accounts.firefox.com/scripts/57f6d4e4.main.js';
const badAbsPath =
'https://accounts.firefox.com/complete_reset_password?token=foo&code=bar&email=a@a.com&service=sync&resume=barbar';
const goodAbsPath =
'https://accounts.firefox.com/complete_reset_password?token=VALUE&code=VALUE&email=VALUE&service=sync&resume=VALUE';
const data = {
culprit: badCulprit,
exception: {
values: [
{
stacktrace: {
frames: [
{
abs_path: badAbsPath, // eslint-disable-line camelcase
},
{
abs_path: badAbsPath, // eslint-disable-line camelcase
},
],
},
},
],
},
request: {
url,
},
type: undefined,
};
const resultData = beforeSend(sentryEnabled, config, data);
expect(
resultData?.exception?.values?.[0].stacktrace?.frames?.[0].abs_path
).toEqual(goodAbsPath);
expect(
resultData?.exception?.values?.[0].stacktrace?.frames?.[1].abs_path
).toEqual(goodAbsPath);
});
});

Просмотреть файл

@ -0,0 +1,65 @@
/* 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/. */
// Change to @sentry/browser after upgrade to Sentry 8
import * as Sentry from '@sentry/nextjs';
import { cleanUpQueryParam } from './cleanUpQueryParam';
import { SentryConfigOpts } from '../models/SentryConfigOpts';
import { tagFxaName } from './tagFxaName';
/**
* 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
*/
export function beforeSend(
sentryEnabled: boolean,
opts: SentryConfigOpts,
event: Sentry.ErrorEvent
) {
if (sentryEnabled === false) {
return null;
}
if (event.request) {
if (event.request.url) {
event.request.url = cleanUpQueryParam(event.request.url);
}
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';
}
}
if (event.exception?.values) {
event.exception.values.forEach((value: Sentry.Exception) => {
if (value.stacktrace && value.stacktrace.frames) {
value.stacktrace.frames.forEach((frame: { abs_path?: string }) => {
if (frame.abs_path) {
frame.abs_path = cleanUpQueryParam(frame.abs_path); // eslint-disable-line camelcase
}
});
}
});
}
if (event.request.headers?.Referer) {
event.request.headers.Referer = cleanUpQueryParam(
event.request.headers.Referer
);
}
}
event = tagFxaName(event, opts.sentry?.clientName || opts.sentry?.serverName);
return event;
}

Просмотреть файл

@ -0,0 +1,34 @@
/* 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 { cleanUpQueryParam } from './cleanUpQueryParam';
describe('cleanUpQueryParam', () => {
it('properly erases sensitive information', () => {
const fixtureUrl1 =
'https://accounts.firefox.com/complete_reset_password?token=foo&code=bar&email=some%40restmail.net';
const expectedUrl1 =
'https://accounts.firefox.com/complete_reset_password?token=VALUE&code=VALUE&email=VALUE';
const resultUrl1 = cleanUpQueryParam(fixtureUrl1);
expect(resultUrl1).toEqual(expectedUrl1);
});
it('properly erases sensitive information, keeps allowed fields', () => {
const fixtureUrl2 =
'https://accounts.firefox.com/signup?client_id=foo&service=sync';
const expectedUrl2 =
'https://accounts.firefox.com/signup?client_id=foo&service=sync';
const resultUrl2 = cleanUpQueryParam(fixtureUrl2);
expect(resultUrl2).toEqual(expectedUrl2);
});
it('properly returns the url when there is no query', () => {
const expectedUrl = 'https://accounts.firefox.com/signup';
const resultUrl = cleanUpQueryParam(expectedUrl);
expect(resultUrl).toEqual(expectedUrl);
});
});

Просмотреть файл

@ -0,0 +1,44 @@
/* 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/. */
/**
* Query parameters we allow to propagate to sentry
*/
const ALLOWED_QUERY_PARAMETERS = [
'automatedBrowser',
'client_id',
'context',
'entrypoint',
'keys',
'migration',
'redirect_uri',
'scope',
'service',
'setting',
'style',
];
/**
* Overwrites sensitive query parameters with a dummy value.
*
* @param url
* @returns url
*/
export function cleanUpQueryParam(url = '') {
const urlObj = new URL(url);
if (!urlObj.search.length) {
return url;
}
// Iterate the search parameters.
urlObj.searchParams.forEach((_, key) => {
if (!ALLOWED_QUERY_PARAMETERS.includes(key)) {
// if the param is a PII (not allowed) then reset the value.
urlObj.searchParams.set(key, 'VALUE');
}
});
return urlObj.href;
}

Просмотреть файл

@ -0,0 +1,10 @@
/* 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/. */
/** Adds fxa.name to data.tags */
export function tagFxaName(data: any, name?: string) {
data.tags = data.tags || {};
data.tags['fxa.name'] = name || 'unknown';
return data;
}

Просмотреть файл

@ -78,6 +78,7 @@
"@radix-ui/react-tooltip": "^1.1.2",
"@sentry/browser": "^7.113.0",
"@sentry/integrations": "^7.113.0",
"@sentry/nextjs": "^8",
"@sentry/node": "^7.113.0",
"@sentry/opentelemetry-node": "^7.113.0",
"@swc/helpers": "0.5.11",
@ -276,7 +277,8 @@
"tap/typescript": "^4.5.2",
"terser:>4.0.0 <5": ">=4.8.1",
"terser:>5 <6": ">=5.14.2",
"underscore": ">=1.13.2"
"underscore": ">=1.13.2",
"@sentry/types": "^7.113.0"
},
"packageManager": "yarn@3.3.0",
"_moduleAliases": {

Просмотреть файл

@ -61,6 +61,7 @@
"@fxa/shared/pem-jwk": ["libs/shared/pem-jwk/src/index.ts"],
"@fxa/shared/react": ["libs/shared/react/src/index.ts"],
"@fxa/shared/sentry": ["libs/shared/sentry/src/index.ts"],
"@fxa/shared/sentry/client": ["libs/shared/sentry/src/client.ts"],
"@fxa/vendored/common-password-list": [
"libs/vendored/common-password-list/src/index.ts"
],

838
yarn.lock

Разница между файлами не показана из-за своего большого размера Загрузить разницу