Skip to content

Commit 4bc524e

Browse files
committed
feat(next): add auth statsd and error page
Because: - In an attempt to have statsd for all 3rd party dependencies, add statsd recording to Auth.js actions and events. - On auth error, users are navigated to Auth.js default error page This commit: - Add statsd counters signin, signout, prompt_none_fail and error - Add custom Auth.js error page Closes #FXA-11670 #FXA-11754
1 parent f4f8654 commit 4bc524e

9 files changed

Lines changed: 190 additions & 2 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Authentication Error page
2+
3+
auth-error-page-title = We Couldn’t Sign You In
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import Image from 'next/image';
6+
import errorIcon from '@fxa/shared/assets/images/error.svg';
7+
import mozillaIcon from '@fxa/shared/assets/images/moz-logo-bw-rgb.svg';
8+
import Link from 'next/link';
9+
import { cookies } from 'next/headers';
10+
import { BaseButton, ButtonVariant } from '@fxa/payments/ui';
11+
import { config } from '../../../../config';
12+
import { getApp } from '@fxa/payments/ui/server';
13+
14+
export default function AuthErrorPage({
15+
searchParams,
16+
}: {
17+
searchParams: Record<string, string>;
18+
}) {
19+
const cookieStore = cookies();
20+
const redirectUrl =
21+
cookieStore.get('__Secure-authjs.callback-url')?.value ||
22+
cookieStore.get('authjs.callback-url')?.value;
23+
const supportUrl = config.supportUrl
24+
const l10n = getApp().getL10n();
25+
getApp().getEmitterService().emit('auth', { type: 'error', errorMessage: searchParams?.error });
26+
27+
return (
28+
<>
29+
<header
30+
className="bg-white fixed flex justify-between items-center shadow h-16 left-0 top-0 mx-auto my-0 px-4 py-0 w-full z-40 tablet:h-20"
31+
role="banner"
32+
>
33+
<div className="flex items-center">
34+
<Image
35+
src={mozillaIcon}
36+
alt="Mozilla logo"
37+
className="object-contain"
38+
width={140}
39+
/>
40+
</div>
41+
</header>
42+
<section
43+
className="flex flex-col items-center text-center max-w-lg mx-auto mt-6 p-16 tablet:my-10 gap-16 bg-white shadow tablet:rounded-xl border border-transparent"
44+
aria-labelledby='unable-to-signin-heading'
45+
>
46+
<h1 id="unable-to-signin-heading" className="text-xl font-bold">
47+
{l10n.getString('auth-error-page-title', 'We Couldn’t Sign You In')}
48+
</h1>
49+
<Image src={errorIcon} alt="" />
50+
<p className="flex flex-col gap-6 items-center text-grey-400 max-w-md text-sm">
51+
<span>
52+
{l10n.getFragmentWithSource('checkout-error-boundary-basic-error-message',
53+
{
54+
elems: {
55+
contactSupportLink: (
56+
<Link href={supportUrl} className="underline hover:text-grey-400">
57+
contact support.
58+
</Link>
59+
)
60+
}
61+
}
62+
,
63+
<>
64+
Something went wrong. Please try again or{' '}
65+
<Link href={supportUrl} className="underline hover:text-grey-400">
66+
contact support.
67+
</Link>
68+
</>
69+
)}
70+
</span>
71+
{redirectUrl &&
72+
<Link href={redirectUrl}>
73+
<BaseButton
74+
variant={ButtonVariant.Primary}
75+
className="text-base"
76+
>
77+
Try again
78+
</BaseButton>
79+
</Link>
80+
}
81+
</p>
82+
</section>
83+
</>
84+
);
85+
}
86+

apps/payments/next/app/api/auth/callback/fxa/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest } from 'next/server';
22
import { GET as AuthJsGET } from '../../../../../auth';
33
import { redirect } from 'next/navigation';
4+
import { getApp } from '@fxa/payments/ui/server';
45

56
export { POST } from '../../../../../auth';
67

@@ -21,6 +22,7 @@ export async function GET(request: NextRequest) {
2122
cookieStore.get('__Secure-authjs.callback-url') ||
2223
cookieStore.get('authjs.callback-url');
2324
if (redirectUrl?.value) {
25+
getApp().getEmitterService().emit('auth', { type: 'prompt_none_fail' });
2426
redirect(redirectUrl.value);
2527
}
2628
}

apps/payments/next/auth.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
import NextAuth from 'next-auth';
66
import { authConfig } from './auth.config';
77
import { config } from './config';
8+
import { BaseError } from '@fxa/shared/error';
9+
10+
import { getApp } from '@fxa/payments/ui/server';
11+
12+
13+
export class AuthError extends BaseError {
14+
constructor(...args: ConstructorParameters<typeof BaseError>) {
15+
super(...args);
16+
this.name = 'AuthError';
17+
Object.setPrototypeOf(this, AuthError.prototype);
18+
}
19+
}
820

921
export const {
1022
handlers: { GET, POST },
@@ -13,6 +25,9 @@ export const {
1325
signOut,
1426
} = NextAuth({
1527
...authConfig,
28+
pages: {
29+
error: '/auth/error',
30+
},
1631
providers: [
1732
{
1833
id: 'fxa',
@@ -56,4 +71,17 @@ export const {
5671
};
5772
},
5873
},
74+
events: {
75+
async signIn() {
76+
getApp().getEmitterService().emit('auth', { type: 'signin' });
77+
},
78+
async signOut() {
79+
getApp().getEmitterService().emit('auth', { type: 'signout' });
80+
}
81+
},
82+
logger: {
83+
error(error: Error) {
84+
console.error(new AuthError(error.message, { cause: error }))
85+
}
86+
}
5987
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
import { BaseError } from "@fxa/shared/error";
5+
6+
export class EmitterServiceHandleAuthError extends BaseError {
7+
constructor(...args: ConstructorParameters<typeof BaseError>) {
8+
super(...args);
9+
this.name = 'EmitterServiceHandleAuthError';
10+
Object.setPrototypeOf(this, EmitterServiceHandleAuthError.prototype);
11+
}
12+
}
13+
14+

libs/payments/events/src/lib/emitter.factories.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
AdditionalMetricsData,
77
SP3RolloutEvent,
88
SubscriptionEndedEvents,
9+
type AuthEvents,
910
} from './emitter.types';
1011
import {
1112
CancellationReason,
@@ -14,6 +15,13 @@ import {
1415
} from '@fxa/payments/metrics';
1516
import { SubplatInterval } from '@fxa/payments/customer';
1617

18+
export const AuthEventsFactory = (
19+
override?: Partial<AuthEvents>
20+
): AuthEvents => ({
21+
type: faker.helpers.arrayElement(['signin', 'signout', 'prompt_none_fail', 'error']),
22+
...override
23+
})
24+
1725
export const AdditionalMetricsDataFactory = (
1826
override?: AdditionalMetricsData
1927
): AdditionalMetricsData => ({

libs/payments/events/src/lib/emitter.service.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { CartManager } from '@fxa/payments/cart';
3737
import { PaymentsEmitterService } from './emitter.service';
3838
import {
3939
AdditionalMetricsDataFactory,
40+
AuthEventsFactory,
4041
SP3RolloutEventFactory,
4142
SubscriptionEndedFactory,
4243
} from './emitter.factories';
@@ -47,6 +48,7 @@ import {
4748
import { AccountManager } from '@fxa/shared/account/account';
4849
import { retrieveAdditionalMetricsData } from './util/retrieveAdditionalMetricsData';
4950
import { Logger } from '@nestjs/common';
51+
import { EmitterServiceHandleAuthError } from './emitter.error';
5052

5153
jest.mock('./util/retrieveAdditionalMetricsData');
5254
const mockedRetrieveAdditionalMetricsData = jest.mocked(
@@ -60,6 +62,7 @@ describe('PaymentsEmitterService', () => {
6062
let productConfigurationManager: ProductConfigurationManager;
6163
let cartManager: CartManager;
6264
let statsd: StatsD;
65+
let logger: Logger;
6366

6467
const additionalMetricsData = AdditionalMetricsDataFactory();
6568
const mockCommonMetricsData = CommonMetricsFactory({
@@ -106,6 +109,7 @@ describe('PaymentsEmitterService', () => {
106109
productConfigurationManager = moduleRef.get(ProductConfigurationManager);
107110
cartManager = moduleRef.get(CartManager);
108111
statsd = moduleRef.get<StatsD>(StatsDService);
112+
logger = moduleRef.get<Logger>(Logger);
109113
});
110114

111115
it('should be defined', () => {
@@ -124,6 +128,30 @@ describe('PaymentsEmitterService', () => {
124128
);
125129
});
126130

131+
describe('handleAuthEvent', () => {
132+
const authEventData = AuthEventsFactory();
133+
beforeEach(() => {
134+
jest.spyOn(statsd, 'increment').mockReturnValue();
135+
});
136+
137+
it('should call manager record method', async () => {
138+
await paymentsEmitterService.handleAuthEvent(authEventData);
139+
140+
expect(statsd.increment).toHaveBeenCalledWith(
141+
`auth_event`,
142+
{ type: authEventData.type }
143+
);
144+
});
145+
146+
it('should log the error if provided', async () => {
147+
const errorMessage = 'Error message text'
148+
const authEventData = AuthEventsFactory({ errorMessage });
149+
await paymentsEmitterService.handleAuthEvent(authEventData);
150+
151+
expect(logger.error).toHaveBeenCalledWith(new EmitterServiceHandleAuthError(errorMessage))
152+
})
153+
});
154+
127155
describe('handleCheckoutView', () => {
128156
beforeEach(() => {
129157
jest

libs/payments/events/src/lib/emitter.service.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
import Emittery from 'emittery';
55
import { ProductConfigurationManager } from '@fxa/shared/cms';
6-
import { Inject, Injectable } from '@nestjs/common';
6+
import { Inject, Injectable, Logger } from '@nestjs/common';
77
import { CartManager, TaxChangeAllowedStatus } from '@fxa/payments/cart';
88
import { PaymentsGleanManager } from '@fxa/payments/metrics';
99
import { LocationStatus } from '@fxa/payments/eligibility';
@@ -13,12 +13,14 @@ import {
1313
PaymentsEmitterEvents,
1414
SP3RolloutEvent,
1515
SubscriptionEndedEvents,
16+
type AuthEvents,
1617
} from './emitter.types';
1718
import { AccountManager } from '@fxa/shared/account/account';
1819
import { retrieveAdditionalMetricsData } from './util/retrieveAdditionalMetricsData';
1920
import { getSubplatInterval } from '@fxa/payments/customer';
2021
import * as Sentry from '@sentry/nestjs';
2122
import { StatsD, StatsDService } from '@fxa/shared/metrics/statsd';
23+
import { EmitterServiceHandleAuthError } from './emitter.error';
2224

2325
@Injectable()
2426
export class PaymentsEmitterService {
@@ -29,7 +31,8 @@ export class PaymentsEmitterService {
2931
private paymentsGleanManager: PaymentsGleanManager,
3032
private cartManager: CartManager,
3133
private accountManager: AccountManager,
32-
@Inject(StatsDService) public statsd: StatsD
34+
@Inject(StatsDService) public statsd: StatsD,
35+
private log: Logger
3336
) {
3437
this.emitter = new Emittery<PaymentsEmitterEvents>();
3538
this.emitter.on('checkoutView', this.handleCheckoutView.bind(this));
@@ -43,12 +46,22 @@ export class PaymentsEmitterService {
4346
);
4447
this.emitter.on('sp3Rollout', this.handleSP3Rollout.bind(this));
4548
this.emitter.on('locationView', this.handleLocationView.bind(this));
49+
this.emitter.on('auth', this.handleAuthEvent.bind(this));
4650
}
4751

4852
getEmitter(): Emittery<PaymentsEmitterEvents> {
4953
return this.emitter;
5054
}
5155

56+
async handleAuthEvent(eventData: AuthEvents) {
57+
const { type, errorMessage } = eventData;
58+
this.statsd.increment('auth_event', { type });
59+
60+
if (errorMessage) {
61+
this.log.error(new EmitterServiceHandleAuthError(errorMessage))
62+
}
63+
}
64+
5265
async handleCheckoutView(eventData: CheckoutEvents) {
5366
const additionalData = await retrieveAdditionalMetricsData(
5467
this.productConfigurationManager,

libs/payments/events/src/lib/emitter.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export type SP3RolloutEvent = {
4444
shadowMode: boolean;
4545
};
4646

47+
export type AuthEvents = {
48+
type: 'signin' | 'signout' | 'prompt_none_fail' | 'error';
49+
errorMessage?: string;
50+
}
51+
4752
export type PaymentsEmitterEvents = {
4853
checkoutView: CheckoutEvents;
4954
checkoutEngage: CheckoutEvents;
@@ -53,6 +58,7 @@ export type PaymentsEmitterEvents = {
5358
subscriptionEnded: SubscriptionEndedEvents;
5459
sp3Rollout: SP3RolloutEvent;
5560
locationView: LocationStatus | TaxChangeAllowedStatus;
61+
auth: AuthEvents;
5662
};
5763

5864
export type AdditionalMetricsData = {

0 commit comments

Comments
 (0)