Merge pull request #13827 from mozilla/revert-13742-FXA-5555-add-logging-to-the-admin-tool

Revert "task(admin-panel): Add event logging for analytics"
This commit is contained in:
Dan Schomburg 2022-08-01 15:36:19 -07:00 коммит произвёл GitHub
Родитель 508e93ecf5 0fdd7e1ad0
Коммит 66b10b6f5e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 39 добавлений и 314 удалений

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

@ -7,7 +7,7 @@ import Chance from 'chance';
import { render, fireEvent, act, screen } from '@testing-library/react';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { CLEAR_BOUNCES_BY_EMAIL } from './Account/index';
import { GET_ACCOUNT_BY_EMAIL, AccountSearch, GET_EMAILS_LIKE } from './index';
import { GET_ACCOUNT_BY_EMAIL, AccountSearch } from './index';
const chance = new Chance();
let testEmail: string;
@ -141,43 +141,11 @@ function exampleBounceMutationResponse(email: string): MockedResponse {
};
}
const MinimalAccountResponse = (testEmail: string) => ({
data: {
accountByEmail: {
uid: '123',
createdAt: 1658534643990,
disabledAt: null,
lockedAt: null,
emails: [
{
email: testEmail,
isVerified: true,
isPrimary: true,
createdAt: 1658534643990,
},
],
emailBounces: [],
securityEvents: [],
totp: [],
recoveryKeys: [],
linkedAccounts: [],
attachedClients: [],
subscriptions: [],
},
},
});
const nextTick = (t?: number) => new Promise((r) => setTimeout(r, t || 0));
beforeEach(() => {
jest.spyOn(window, 'confirm').mockImplementation(() => true);
testEmail = chance.email();
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders without imploding', () => {
const renderResult = render(<AccountSearch />);
const getByTestId = renderResult.getByTestId;
@ -185,114 +153,6 @@ it('renders without imploding', () => {
expect(getByTestId('search-form')).toBeInTheDocument();
});
it('calls account search', async () => {
let calledAccountSearch = false;
const mocks = [
{
request: {
query: GET_ACCOUNT_BY_EMAIL,
variables: {
email: testEmail,
autoCompleted: false,
},
},
result: () => {
calledAccountSearch = true;
return MinimalAccountResponse(testEmail);
},
},
];
const renderResult = render(
<MockedProvider mocks={mocks} addTypename={false}>
<AccountSearch />
</MockedProvider>
);
await act(async () => {
fireEvent.change(renderResult.getByTestId('email-input'), {
target: { value: testEmail },
});
fireEvent.blur(renderResult.getByTestId('email-input'));
});
await nextTick();
await act(async () => {
fireEvent.click(renderResult.getByTestId('search-button'));
});
await nextTick();
expect(renderResult.getByTestId('account-section')).toBeInTheDocument();
expect(calledAccountSearch).toBeTruthy();
});
it('auto completes', async () => {
let calledAccountSearch = false;
let calledGetEmailsLike = false;
const mocks = [
{
request: {
query: GET_EMAILS_LIKE,
variables: {
search: testEmail.substring(0, 6),
},
},
result: () => {
calledGetEmailsLike = true;
return {
data: {
getEmailsLike: [{ email: testEmail }],
},
};
},
},
{
request: {
query: GET_ACCOUNT_BY_EMAIL,
variables: {
email: testEmail,
autoCompleted: true,
},
},
result: () => {
calledAccountSearch = true;
return MinimalAccountResponse(testEmail);
},
},
];
const renderResult = render(
<MockedProvider mocks={mocks} addTypename={false}>
<AccountSearch />
</MockedProvider>
);
await act(async () => {
fireEvent.change(renderResult.getByTestId('email-input'), {
target: { value: testEmail.substring(0, 6) },
});
fireEvent.blur(renderResult.getByTestId('email-input'));
});
await nextTick();
await act(async () => {
fireEvent.click(
renderResult.getByTestId('email-suggestions').getElementsByTagName('a')[0]
);
});
await nextTick();
await act(async () => {
fireEvent.click(renderResult.getByTestId('search-button'));
});
await nextTick();
expect(calledGetEmailsLike).toBeTruthy();
expect(calledAccountSearch).toBeTruthy();
expect(renderResult.getByTestId('email-input')).toHaveValue(testEmail);
expect(renderResult.getByTestId('account-section')).toBeInTheDocument();
});
// FIXME: this test is flaky
it.skip('displays the account email bounces, and can clear them', async () => {
const hasResultsAccountResponse = exampleAccountResponse(testEmail);

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

@ -84,8 +84,8 @@ const ACCOUNT_SCHEMA = `
}
`;
export const GET_ACCOUNT_BY_EMAIL = gql`
query getAccountByEmail($email: String!, $autoCompleted: Boolean!) {
accountByEmail(email: $email, autoCompleted:$autoCompleted) {
query getAccountByEmail($email: String!) {
accountByEmail(email: $email) {
${ACCOUNT_SCHEMA}
}
}
@ -117,11 +117,9 @@ export const AccountSearch = () => {
const [showResult, setShowResult] = useState<boolean>(false);
const [showSuggestion, setShowSuggestion] = useState<boolean>(false);
const [searchInput, setSearchInput] = useState<string>('');
const [selectedSuggestion, setSelectedSuggestion] = useState<string>('');
// define two queries to search by either email or uid.
const [getAccountByEmail, emailResults] = useLazyQuery(GET_ACCOUNT_BY_EMAIL);
const [getAccountByUID, uidResults] = useLazyQuery(GET_ACCOUNT_BY_UID);
const [getAccountbyEmail, emailResults] = useLazyQuery(GET_ACCOUNT_BY_EMAIL);
const [getAccountbyUID, uidResults] = useLazyQuery(GET_ACCOUNT_BY_UID);
// choose which query result to show based on type of query made
const [isEmail, setIsEmail] = useState<boolean>(false);
const queryResults = isEmail && showResult ? emailResults : uidResults;
@ -132,13 +130,10 @@ export const AccountSearch = () => {
const trimmedSearchInput = searchInput.trim();
event.preventDefault();
const isUID = validateUID(trimmedSearchInput);
// choose correct query if email or uid
if (isUID) {
// uid and non-empty
getAccountByUID({
variables: { uid: trimmedSearchInput, autoCompleted: false },
});
getAccountbyUID({ variables: { uid: trimmedSearchInput } });
setIsEmail(false);
setShowResult(true);
} else if (
@ -147,12 +142,7 @@ export const AccountSearch = () => {
trimmedSearchInput !== ''
) {
// assume email if not uid and non-empty; must at least have '@'
getAccountByEmail({
variables: {
email: trimmedSearchInput,
autoCompleted: selectedSuggestion === trimmedSearchInput,
},
});
getAccountbyEmail({ variables: { email: trimmedSearchInput } });
setIsEmail(true);
setShowResult(true);
}
@ -188,7 +178,6 @@ export const AccountSearch = () => {
const suggestionSelected = (value: string) => {
setSearchInput(value);
setSelectedSuggestion(value);
setShowSuggestion(false);
};
@ -262,10 +251,7 @@ export const AccountSearch = () => {
/>
</button>
{showSuggestion && filteredList.length > 0 && (
<div
className="suggestions-list absolute top-full w-full bg-white border border-grey-100 mt-3 shadow-sm rounded overflow-hidden"
data-testid="email-suggestions"
>
<div className="suggestions-list absolute top-full w-full bg-white border border-grey-100 mt-3 shadow-sm rounded overflow-hidden">
{renderSuggestions()}
</div>
)}

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

@ -20,7 +20,6 @@ import { UserGroupGuard } from './auth/user-group-header.guard';
import Config, { AppConfig } from './config';
import { DatabaseModule } from './database/database.module';
import { DatabaseService } from './database/database.service';
import { EventLoggingModule } from './event-logging/event-logging.module';
import { GqlModule } from './gql/gql.module';
import { SubscriptionModule } from './subscriptions/subscriptions.module';
@ -33,7 +32,6 @@ const version = getVersionInfo(__dirname);
isGlobal: true,
}),
DatabaseModule,
EventLoggingModule,
SubscriptionModule,
GqlModule,
GraphQLModule.forRootAsync({

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

@ -0,0 +1,17 @@
{
"subscriptions": {
"enabled": true,
"sharedSecret": "devsecret",
"paymentsServer": {
"url": "http://localhost:3031/"
},
"stripeApiKey": "sk-test_123"
},
"featureFlags": {
"subscriptions": {
"playStore": true,
"appStore": true,
"stripe": true
}
}
}

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

@ -1,12 +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 { Module } from '@nestjs/common';
import { EventLoggingService } from './event-logging.service';
@Module({
providers: [EventLoggingService],
exports: [EventLoggingService],
})
export class EventLoggingModule {}

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

@ -1,42 +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 { Test, TestingModule } from '@nestjs/testing';
import { MockLogService, logger } from '../mocks';
import { EventLoggingService, EventNames } from './event-logging.service';
describe('EventLogging', () => {
let eventLogging: EventLoggingService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EventLoggingService, MockLogService],
}).compile();
eventLogging = module.get<EventLoggingService>(EventLoggingService);
});
beforeEach(() => {
logger.info.mockClear();
});
it('should be defined', () => {
expect(eventLogging).toBeDefined();
});
it('should record event', () => {
eventLogging.onEvent(EventNames.ClearBounces);
expect(logger.info).lastCalledWith('admin-panel-events', {
event: 'clear-bounces',
});
});
it('should record account search event with flag', () => {
eventLogging.onAccountSearch('email', true);
expect(logger.info).lastCalledWith('admin-panel-events', {
event: 'account-search',
'search-type': 'email',
'auto-completed': true,
});
});
});

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

@ -1,50 +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 { Injectable } from '@nestjs/common';
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
/** Known event names */
export enum EventNames {
ClearBounces = 'clear-bounces',
UnverifyEmail = 'unverify-email',
DisableLogin = 'disable-login',
AccountSearch = 'account-search',
}
/**
* Handles logging of pertinent events.
*/
@Injectable()
export class EventLoggingService {
private readonly logType = 'admin-panel-events';
/**
* Creates new event logger
* @param log
*/
constructor(private readonly log: MozLoggerService) {}
/**
* Logs an event occurrence
* @param eventName - A known event name
*/
public onEvent(eventName: Omit<EventNames, EventNames.AccountSearch>) {
this.log.info(this.logType, {
event: eventName,
});
}
/**
* Records an AccountSearch event and indicates if the query was the result of an autocompletion.
* @param autocompleted
*/
public onAccountSearch(searchType: 'email' | 'uid', autoCompleted: boolean) {
this.log.info(this.logType, {
event: EventNames.AccountSearch,
'search-type': searchType,
'auto-completed': autoCompleted,
});
}
}

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

@ -37,7 +37,6 @@ import {
randomTotp,
} from 'fxa-shared/test/db/models/auth/helpers';
import { Knex } from 'knex';
import { EventLoggingService } from '../../event-logging/event-logging.service';
import { SubscriptionsService } from '../../subscriptions/subscriptions.service';
import { AccountResolver } from './account.resolver';
@ -146,7 +145,6 @@ describe('AccountResolver', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AccountResolver,
EventLoggingService,
MockMozLogger,
MockConfig,
MockMetricsFactory,
@ -170,7 +168,7 @@ describe('AccountResolver', () => {
const result = (await resolver.accountByUid(USER_1.uid, 'joe')) as Account;
expect(result).toBeDefined();
expect(result.email).toBe(USER_1.email);
expect(logger.info).toBeCalledTimes(2);
expect(logger.info).toBeCalledTimes(1);
});
it('disables an account by uid', async () => {
@ -182,7 +180,7 @@ describe('AccountResolver', () => {
)) as Account;
expect(updatedResult).toBeDefined();
expect(updatedResult.disabledAt).not.toBeNull();
expect(logger.info).toBeCalledTimes(3);
expect(logger.info).toBeCalledTimes(1);
});
it('enables an account by uid', async () => {
@ -194,36 +192,34 @@ describe('AccountResolver', () => {
)) as Account;
expect(updatedResult).toBeDefined();
expect(updatedResult.disabledAt).toBeNull();
expect(logger.info).toBeCalledTimes(2);
expect(logger.info).toBeCalledTimes(1);
});
it('does not locate non-existent users by uid', async () => {
const result = await resolver.accountByUid(USER_2.uid, 'joe');
expect(result).toBeUndefined();
expect(logger.info).toBeCalledTimes(2);
expect(logger.info).toBeCalledTimes(1);
});
it('locates the user by email', async () => {
const result = (await resolver.accountByEmail(
USER_1.email,
true,
'joe'
)) as Account;
expect(result).toBeDefined();
expect(result.email).toBe(USER_1.email);
expect(logger.info).toBeCalledTimes(2);
expect(logger.info).toBeCalledTimes(1);
});
it('does not locate non-existent users by email', async () => {
const result = await resolver.accountByEmail(USER_2.email, true, 'joe');
const result = await resolver.accountByEmail(USER_2.email, 'joe');
expect(result).toBeUndefined();
expect(logger.info).toBeCalledTimes(2);
expect(logger.info).toBeCalledTimes(1);
});
it('loads emailBounces', async () => {
const user = (await resolver.accountByEmail(
USER_1.email,
true,
'joe'
)) as Account;
const { emailType: templateName } = await db.emailTypes
@ -241,7 +237,6 @@ describe('AccountResolver', () => {
it('loads emails', async () => {
const user = (await resolver.accountByEmail(
USER_1.email,
true,
'joe'
)) as Account;
const result = await resolver.emails(user);
@ -252,7 +247,6 @@ describe('AccountResolver', () => {
it('loads totp', async () => {
const user = (await resolver.accountByEmail(
USER_1.email,
true,
'joe'
)) as Account;
const result = await resolver.totp(user);
@ -264,7 +258,6 @@ describe('AccountResolver', () => {
it('loads recoveryKeys', async () => {
const user = (await resolver.accountByEmail(
USER_1.email,
true,
'joe'
)) as Account;
const result = await resolver.recoveryKeys(user);
@ -276,7 +269,6 @@ describe('AccountResolver', () => {
it('loads attached clients', async () => {
const user = (await resolver.accountByEmail(
USER_1.email,
true,
'joe'
)) as Account;
const result = await resolver.attachedClients(user);
@ -301,7 +293,6 @@ describe('AccountResolver', () => {
it('loads linkedAccounts', async () => {
const user = (await resolver.accountByEmail(
USER_1.email,
true,
'joe'
)) as Account;
const result = await resolver.linkedAccounts(user);

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

@ -28,10 +28,6 @@ import { Features } from '../../auth/user-group-header.decorator';
import { AppConfig } from '../../config';
import { DatabaseService } from '../../database/database.service';
import { uuidTransformer } from '../../database/transformers';
import {
EventLoggingService,
EventNames,
} from '../../event-logging/event-logging.service';
import { Account as AccountType } from '../../gql/model/account.model';
import { AttachedClient } from '../../gql/model/attached-clients.model';
import { Email as EmailType } from '../../gql/model/emails.model';
@ -84,8 +80,7 @@ export class AccountResolver {
private log: MozLoggerService,
private db: DatabaseService,
private subscriptionsService: SubscriptionsService,
private configService: ConfigService<AppConfig>,
private eventLogging: EventLoggingService
private configService: ConfigService<AppConfig>
) {}
@Features(AdminPanelFeature.AccountSearch)
@ -94,7 +89,6 @@ export class AccountResolver {
@Args('uid', { nullable: false }) uid: string,
@CurrentUser() user: string
) {
this.eventLogging.onAccountSearch('uid', false);
let uidBuffer;
try {
uidBuffer = uuidTransformer.to(uid);
@ -112,10 +106,8 @@ export class AccountResolver {
@Query((returns) => AccountType, { nullable: true })
public accountByEmail(
@Args('email', { nullable: false }) email: string,
@Args('autoCompleted', { nullable: false }) autoCompleted: boolean,
@CurrentUser() user: string
) {
this.eventLogging.onAccountSearch('email', autoCompleted);
this.log.info('accountByEmail', { email, user });
return this.db.account
.query()
@ -139,7 +131,6 @@ export class AccountResolver {
@Features(AdminPanelFeature.UnverifyEmail)
@Mutation((returns) => Boolean)
public async unverifyEmail(@Args('email') email: string) {
this.eventLogging.onEvent(EventNames.UnverifyEmail);
const result = await this.db.emails
.query()
.where('normalizedEmail', 'like', `${email.toLowerCase()}%`)
@ -153,7 +144,6 @@ export class AccountResolver {
@Features(AdminPanelFeature.DisableAccount)
@Mutation((returns) => Boolean)
public async disableAccount(@Args('uid') uid: string) {
this.eventLogging.onEvent(EventNames.DisableLogin);
const uidBuffer = uuidTransformer.to(uid);
const result = await this.db.account
.query()

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

@ -14,7 +14,6 @@ import {
randomEmailBounce,
} from 'fxa-shared/test/db/models/auth/helpers';
import { Knex } from 'knex';
import { EventLoggingService } from '../../event-logging/event-logging.service';
import { EmailBounceResolver } from './email-bounce.resolver';
@ -63,7 +62,6 @@ describe('EmailBounceResolver', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EmailBounceResolver,
EventLoggingService,
MockMozLogger,
MockConfig,
MockMetricsFactory,
@ -85,6 +83,6 @@ describe('EmailBounceResolver', () => {
it('should clear email bounces', async () => {
const result = await resolver.clearEmailBounce(USER_1.email, 'test');
expect(result).toBeTruthy();
expect(logger.info).toBeCalledTimes(2);
expect(logger.info).toBeCalledTimes(1);
});
});

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

@ -11,19 +11,11 @@ import { GqlAuthHeaderGuard } from '../../auth/auth-header.guard';
import { DatabaseService } from '../../database/database.service';
import { EmailBounce as EmailBounceType } from '../../gql/model/email-bounces.model';
import { AdminPanelFeature } from 'fxa-shared/guards';
import {
EventLoggingService,
EventNames,
} from '../../event-logging/event-logging.service';
@UseGuards(GqlAuthHeaderGuard)
@Resolver((of: any) => EmailBounceType)
export class EmailBounceResolver {
constructor(
private log: MozLoggerService,
private db: DatabaseService,
private eventLogging: EventLoggingService
) {}
constructor(private log: MozLoggerService, private db: DatabaseService) {}
@Features(AdminPanelFeature.ClearEmailBounces)
@Mutation((returns) => Boolean)
@ -31,7 +23,6 @@ export class EmailBounceResolver {
@Args('email') email: string,
@CurrentUser() user: string
) {
this.eventLogging.onEvent(EventNames.ClearBounces);
const result = await this.db.emailBounces
.query()
.delete()

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

@ -4,14 +4,13 @@
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { EventLoggingModule } from '../event-logging/event-logging.module';
import { SubscriptionModule } from '../subscriptions/subscriptions.module';
import { AccountResolver } from './account/account.resolver';
import { EmailBounceResolver } from './email-bounce/email-bounce.resolver';
import { RelyingPartyResolver } from './relying-party/relying-party.resolver';
@Module({
imports: [DatabaseModule, SubscriptionModule, EventLoggingModule],
imports: [DatabaseModule, SubscriptionModule],
providers: [AccountResolver, EmailBounceResolver, RelyingPartyResolver],
})
export class GqlModule {}

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

@ -159,7 +159,7 @@ export interface RelyingParty {
export interface IQuery {
accountByUid(uid: string): Nullable<Account> | Promise<Nullable<Account>>;
accountByEmail(autoCompleted: boolean, email: string): Nullable<Account> | Promise<Nullable<Account>>;
accountByEmail(email: string): Nullable<Account> | Promise<Nullable<Account>>;
getEmailsLike(search: string): Nullable<Email[]> | Promise<Nullable<Email[]>>;
relyingParties(): RelyingParty[] | Promise<RelyingParty[]>;
}

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

@ -154,7 +154,7 @@ type RelyingParty {
type Query {
accountByUid(uid: String!): Account
accountByEmail(autoCompleted: Boolean!, email: String!): Account
accountByEmail(email: String!): Account
getEmailsLike(search: String!): [Email!]
relyingParties: [RelyingParty!]!
}

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

@ -36,8 +36,7 @@ describe('AppController (e2e)', () => {
.send({
operationName: null,
variables: {},
query:
'{accountByEmail(email:"test@test.com", autoCompleted:true){uid}}',
query: '{accountByEmail(email:"test@test.com"){uid}}',
})
.expect(200);
});