Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into dependabot/npm_and_ya…
Browse files Browse the repository at this point in the history
…rn/react-scripts-5.0.0
  • Loading branch information
haworku committed Mar 28, 2022
2 parents 2fe9b73 + 2436768 commit 1853ab3
Show file tree
Hide file tree
Showing 19 changed files with 487 additions and 107 deletions.
48 changes: 48 additions & 0 deletions docs/ADRs/016-use-otel-for-monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 016 - Use OTEL for monitoring via New Relic

We need a way to gather performance data and monitor our application. We also would like to receive alerts for particular error conditions so that we can investigate errors before they are reported by users. This means we need to choose a monitoring and observability library and backend to collect this data.

## Considered Options

### Open Telemetry

Open Telemetry (OTEL) is an open source standard for application instrumentation. It consists of APIs, SDKs and tools for developers to instrument code in a standardized way. The protocol itself “describes the encoding, transport, and delivery mechanism of telemetry data between telemetry sources, intermediate nodes such as collectors and telemetry backends.”

OTEL in a basic deployment consists of language libraries to instrument your code, an OTEL Collector which can receive the instrumented code, and an OTEL Exporter which sends the data to a configured backend. There are various open source backends (Jaeger, Zipkin) and many established monitoring services now support injesting OTEL metrics (New Relic, Honeycomb).

### New Relic APM

New Relic APM is an application performance monitoring SaaS platform. It consists of language libraries that auto instrument your code and display application performance metrics. The user typically does not write instrumentation on their own and instead relies on the auto-instrumentation.

### Honeycomb

Honeycomb is a datastore and query engine for observability data. It relies on OTEL libraries to do the instrumenting of your code and provides a datastore and query engine to explore the exported data.

## Chosen Decision: Use OTEL with New Relic as the backend datastore.

This decision really came down to the fact that CMS has a contract with New Relic and it is available to our team without needing to do any license aquisition. Since New Relic supports OTEL, we've decided to use OTEL instrumentation over New Relic's APM as we're seeing more of the monitoring and observability world embrace OTEL. This will allow us to choose a different backend in the future if we for any reason need to move providers. Choosing OTEL also pushes us to be more explicit about parts of the application we'd like to monitor, rather than being limited to only what auto instrumentation gives us from a product like NR APM.

By combining OTEL with New Relic we also can use other New Relic features, like AWS infrastructure monitoring, uptime ping metrics, etc.

### Pros/Cons

#### OTEL

- `+` Open Source solution that many monitoring vendors are standardizing on.
- `+` Allows for both auto instrumentation and custom instrumentation.
- `+` AWS provides a lambda layer that is easy to setup and get OTEL stats collected and exported.
- `+` OSS backends like Jaeger allow us to run traces in local dev environments.
- `-` High learning curve for our team to begin using.
- `-` Not all parts of the OTEL standards are stable, particularly in metrics and logs.

#### New Relic APM

- `+` Easy to install and configure. Not much of a learning curve to get started.
- `+` CMS already gives teams access to it.
- `-` If we write custom traces for NR APM then that code only works for NR backend.

#### Honeycomb

- `+` Powerful query interface.
- `+` Supports OTEL.
- `-` CMS does not have a contract and we'd have to aquire a license on our own.
34 changes: 18 additions & 16 deletions services/app-api/resolvers/unlockStateSubmission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,16 @@ describe('unlockStateSubmission', () => {

expect(unlockedSub.revisions[0].revision.submitInfo).toBeNull()
expect(unlockedSub.revisions[1].revision.submitInfo).toBeDefined()
expect(unlockedSub.revisions[1].revision.submitInfo?.updatedAt).toEqual(todaysDate())
expect(unlockedSub.revisions[1].revision.submitInfo?.updatedAt.toISOString()).toContain(todaysDate())
// check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z
expect(unlockedSub.revisions[1].revision.submitInfo?.updatedAt.toISOString()).toContain('Z')

expect(unlockedSub.revisions[0].revision.unlockInfo).toBeDefined()
expect(unlockedSub.revisions[0].revision.unlockInfo).toEqual({
updatedAt: todaysDate(),
updatedBy: 'zuko@example.com',
updatedReason: 'Super duper good reason.'
})
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedBy).toEqual('zuko@example.com')
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedReason).toEqual('Super duper good reason.')
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedAt.toISOString()).toContain(todaysDate())
// check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedAt.toISOString()).toContain('Z')
})

it('returns a DraftSubmission that can be updated without errors', async () => {
Expand Down Expand Up @@ -103,11 +105,11 @@ describe('unlockStateSubmission', () => {
// After unlock, we should get a draft submission back
expect(unlockedSub.status).toEqual('UNLOCKED')
expect(unlockedSub.revisions[0].revision.unlockInfo).toBeDefined()
expect(unlockedSub.revisions[0].revision.unlockInfo).toEqual({
updatedAt: todaysDate(),
updatedBy: 'zuko@example.com',
updatedReason: 'Super duper good reason.'
})
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedBy).toEqual('zuko@example.com')
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedReason).toEqual('Super duper good reason.')
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedAt.toISOString()).toContain(todaysDate())
// check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z
expect(unlockedSub.revisions[0].revision.unlockInfo?.updatedAt.toISOString()).toContain('Z')

// after unlock we should be able to update that draft submission and get the results
const updates = {
Expand Down Expand Up @@ -157,11 +159,11 @@ describe('unlockStateSubmission', () => {

const draft = await unlockTestDraftSubmission(cmsServer, stateSubmission.id, 'Very super duper good reason.')
expect(draft.status).toEqual('UNLOCKED')
expect(draft.revisions[0].revision.unlockInfo).toEqual({
updatedAt: todaysDate(),
updatedBy: 'zuko@example.com',
updatedReason: 'Very super duper good reason.'
})
expect(draft.revisions[0].revision.unlockInfo?.updatedBy).toEqual('zuko@example.com')
expect(draft.revisions[0].revision.unlockInfo?.updatedReason).toEqual('Very super duper good reason.')
expect(draft.revisions[0].revision.unlockInfo?.updatedAt.toISOString()).toContain(todaysDate())
// check that the date has full ISO time eg. 2022-03-25T03:09:54.864Z
expect(draft.revisions[0].revision.unlockInfo?.updatedAt.toISOString()).toContain('Z')
})

it('returns errors if a state user tries to unlock', async () => {
Expand Down
7 changes: 2 additions & 5 deletions services/app-graphql/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ type Query {
indexSubmissions: IndexSubmissionsPayload!

# new api
fetchSubmission2(
input: FetchSubmission2Input!
): FetchSubmission2Payload!
fetchSubmission2(input: FetchSubmission2Input!): FetchSubmission2Payload!
indexSubmissions2: IndexSubmissions2Payload!

}

type Mutation {
Expand Down Expand Up @@ -160,7 +157,7 @@ type Submission2 {
}

type UpdateInformation {
updatedAt: Date!
updatedAt: DateTime!
updatedBy: String!
updatedReason: String!
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@import '../../styles/uswdsImports.scss';
@import '../../styles/custom.scss';

.summarySection {
background: $cms-color-white;
line-height: units(3);
@include u-radius('md');

h2 {
margin: 0;
@include u-text('normal');
}
&:first-of-type {
h2 {
@include u-text('bold');
font-size: size('body', 'lg');
}
}
}

.tag {
font-weight: bold;
}

.accordionRows {
font-size: 40px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ChangeHistory } from './ChangeHistory'
import { Submission2 } from '../../gen/gqlClient'

export default {
title: 'Components/ChangeHistory',
component: ChangeHistory,
}

const submissionData: Submission2 = {
id: '440d6a53-bb0a-49ae-9a9c-da7c5352789f',
stateCode: 'MN',
status: 'RESUBMITTED',
intiallySubmittedAt: '2022-03-23',
revisions: [
{
revision: {
id: '26596de8-852d-4e42-bb0a-c9c9bf78c3de',
unlockInfo: {
updatedAt: '2022-03-24T01:18:44.663Z',
updatedBy: 'zuko@example.com',
updatedReason: 'testing stuff',
__typename: 'UpdateInformation',
},
submitInfo: {
updatedAt: '2022-03-24T01:19:46.154Z',
updatedBy: 'aang@example.com',
updatedReason: 'Placeholder resubmission reason',
__typename: 'UpdateInformation',
},
createdAt: '2022-03-24T01:18:44.665Z',
submissionData: 'alkdfjlasdjf',
__typename: 'Revision',
},
__typename: 'RevisionEdge',
},
{
revision: {
id: 'e048cdcf-5b19-4acb-8ead-d7dc2fd6cd30',
unlockInfo: null,
submitInfo: {
updatedAt: '2022-03-23T02:08:52.259Z',
updatedBy: 'aang@example.com',
updatedReason: 'Initial submission',
__typename: 'UpdateInformation',
},
createdAt: '2022-03-23T02:08:14.241Z',
submissionData: 'weoirna;dfkl',
__typename: 'Revision',
},
__typename: 'RevisionEdge',
},
],
__typename: 'Submission2',
}

export const DemoListUploadSuccess = (): React.ReactElement => {
return <ChangeHistory submission={submissionData} />
}
111 changes: 111 additions & 0 deletions services/app-web/src/components/ChangeHistory/ChangeHistory.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ChangeHistory } from './ChangeHistory'
import { Submission2 } from '../../gen/gqlClient'

const submissionData: Submission2 = {
id: '440d6a53-bb0a-49ae-9a9c-da7c5352789f',
stateCode: 'MN',
status: 'RESUBMITTED',
intiallySubmittedAt: '2022-03-23',
revisions: [
{
revision: {
id: '26596de8-852d-4e42-bb0a-c9c9bf78c3de',
unlockInfo: {
updatedAt: '2022-03-24T01:18:44.663Z',
updatedBy: 'zuko@example.com',
updatedReason: 'testing stuff',
__typename: 'UpdateInformation',
},
submitInfo: {
updatedAt: '2022-03-24T01:19:46.154Z',
updatedBy: 'aang@example.com',
updatedReason: 'Placeholder resubmission reason',
__typename: 'UpdateInformation',
},
createdAt: '2022-03-24T01:18:44.665Z',
submissionData: 'qpoiuenad',
__typename: 'Revision',
},
__typename: 'RevisionEdge',
},
{
revision: {
id: 'e048cdcf-5b19-4acb-8ead-d7dc2fd6cd30',
unlockInfo: null,
submitInfo: {
updatedAt: '2022-03-23T02:08:52.259Z',
updatedBy: 'aang@example.com',
updatedReason: 'Initial submission',
__typename: 'UpdateInformation',
},
createdAt: '2022-03-23T02:08:14.241Z',
submissionData: 'nmzxcv;lasf',
__typename: 'Revision',
},
__typename: 'RevisionEdge',
},
],
__typename: 'Submission2',
}

describe('Change History', () => {
it('renders without errors', () => {
render(<ChangeHistory submission={submissionData} />)
expect(screen.getByText('Change history')).toBeInTheDocument()
})

it('includes an accordion list of changes', () => {
render(<ChangeHistory submission={submissionData} />)
expect(screen.getByTestId('accordion')).toBeInTheDocument()
})

it('has expected text in the accordion title', () => {
render(<ChangeHistory submission={submissionData} />)
expect(
screen.getByRole('button', {
name: '03/23/22 9:19pm ET - Submission',
})
).toBeInTheDocument()
})

it('has expected text in the accordion content', () => {
render(<ChangeHistory submission={submissionData} />)
expect(
screen.getByText('Placeholder resubmission reason')
).toBeInTheDocument()
})

it('should expand and collapse the accordion on click', () => {
render(<ChangeHistory submission={submissionData} />)
expect(
screen.getByText('Placeholder resubmission reason')
).not.toBeVisible()
const accordionRows = screen.getAllByRole('button')
userEvent.click(accordionRows[0])
expect(
screen.getByText('Placeholder resubmission reason')
).toBeVisible()
userEvent.click(accordionRows[0])
expect(
screen.getByText('Placeholder resubmission reason')
).not.toBeVisible()
})
it('should list the submission events in reverse chronological order', () => {
render(<ChangeHistory submission={submissionData} />)
expect(
screen.getByText('Placeholder resubmission reason')
).not.toBeVisible()
const accordionRows = screen.getAllByRole('button')
expect(accordionRows[0]).toHaveTextContent(
'03/23/22 9:19pm ET - Submission'
)
expect(accordionRows[1]).toHaveTextContent(
'03/23/22 9:18pm ET - Unlock'
)
expect(accordionRows[2]).toHaveTextContent(
'03/22/22 10:08pm ET - Submission'
)
})
})
Loading

0 comments on commit 1853ab3

Please sign in to comment.