Access NerdStorageVault from your Nerdlet

Throughout this course, you're building a New Relic One application that gathers telemetry data from a demo web service that's running an A/B test on a newsletter signup form. The purpose of your New Relic One application is to help you understand how design changes impact the number of high quality newsletter subscriptions your service gets. The business objective, to increase your service's high quality newsletter subscriptions, relies primarily on three key pieces of information:

  • Number of page views per version
  • Number of subscriptions per version
  • Number of cancellations

Cancellations are important because if one design version of your newsletter signup form results in a high number of subscriptions but also a high number of cancellations, then those subscriptions are not as valuable.

In previous lessons, you gathered data for page views and subscriptions from New Relic's database (NRDB), but you still need cancellation data. Your demo application does not report cancellation data to New Relic, so you need to fetch that data from an external source. We've provided a service at https://api.nerdsletter.net/cancellations to return fake cancellation data for the purposes of this course. If you visit this URL in your browser, you'll see a brief message: "Unauthorized". This is because we created this service with a requirement that whoever requests its data must pass an Authorization header with the bearer token ABC123.

So before you can request cancellation data from api.nerdsletter.net, you need to implement a few new behaviors in your application:

  • Provide a mechanism for inputting an authorization token
  • Persist the authorization token in a secure data store

To input your authorization token, you'll use a Modal with a TextField. The secure data store you'll use is called NerdStorageVault. It's different from NerdStorage, which you used in the last lesson, because it only supports user storage and encrypts its data.

Store your API token

Step 1 of 3

Change to the add-nerdstoragevault/ab-test directory of the coursework repository:

bash
$
cd nru-programmability-course/add-nerdstoragevault/ab-test

This directory contains the code that we expect your application to have at this point in the course. By navigating to the correct directory at the start of each lesson, you leave your custom code behind, thereby protecting yourself from carrying incorrect code from one lesson to the next.

Step 2 of 3

In your Nerdlet's index.js file, initialize state in AbTestNerdletNerdlet with a null token default:

import React from 'react';
import { ChartGroup, Grid, GridItem } from 'nr1';
import EndTestSection from './end-test';
import NewsletterSignups from './newsletter-signups';
import PastTests from './past-tests';
import TotalCancellations from './total-cancellations';
import TotalSubscriptions from './total-subscriptions';
import VersionDescription from './description';
import VersionPageViews from './page-views';
import VersionTotals from './totals';
const ACCOUNT_ID = 123456 // <YOUR NEW RELIC ACCOUNT ID>
const VERSION_A_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter"'
const VERSION_B_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter and get a free shirt!"'
export default class AbTestNerdletNerdlet extends React.Component {
constructor() {
super(...arguments);
this.state = {
token: null,
}
}
render() {
return <div>
<Grid className="wrapper">
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_A_DESCRIPTION}
version="A"
/>
</GridItem>
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_B_DESCRIPTION}
version="B"
/>
</GridItem>
<GridItem columnSpan={12}><hr /></GridItem>
<GridItem columnSpan={12}><NewsletterSignups /></GridItem>
<GridItem columnSpan={6}><TotalSubscriptions /></GridItem>
<GridItem columnSpan={6}><TotalCancellations /></GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='a' accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='b' accountId={ACCOUNT_ID} />
</GridItem>
<ChartGroup>
<GridItem columnSpan={6}>
<VersionPageViews version='a' />
</GridItem>
<GridItem columnSpan={6}>
<VersionPageViews version='b' />
</GridItem>
</ChartGroup>
<GridItem columnSpan={12}>
<EndTestSection
accountId={ACCOUNT_ID}
versionADescription={VERSION_A_DESCRIPTION}
versionBDescription={VERSION_B_DESCRIPTION}
/>
</GridItem>
<GridItem columnSpan={12}>
<PastTests accountId={ACCOUNT_ID} />
</GridItem>
</Grid>
</div>
}
}
nerdlets/ab-test-nerdlet/index.js

Your Nerdlet uses this token state to manage the authorization token you'll later pass to the third-party service. However, a component's state is not a long-term solution for data management. For that, you need NerdStorageVault.

Step 3 of 3

Implement a method, called storeToken(), which mutates NerdStorageVault data, and bind that method to the AbTestNerdletNerdlet instance:

import React from 'react';
import {
ChartGroup,
Grid,
GridItem,
NerdGraphMutation,
} from 'nr1';
import EndTestSection from './end-test';
import NewsletterSignups from './newsletter-signups';
import PastTests from './past-tests';
import TotalCancellations from './total-cancellations';
import TotalSubscriptions from './total-subscriptions';
import VersionDescription from './description';
import VersionPageViews from './page-views';
import VersionTotals from './totals';
const ACCOUNT_ID = 123456 // <YOUR NEW RELIC ACCOUNT ID>
const VERSION_A_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter"'
const VERSION_B_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter and get a free shirt!"'
export default class AbTestNerdletNerdlet extends React.Component {
constructor() {
super(...arguments);
this.state = {
token: null,
}
this.storeToken = this.storeToken.bind(this);
}
storeToken(newToken) {
if (newToken != this.state.token) {
const mutation = `
mutation($key: String!, $token: SecureValue!) {
nerdStorageVaultWriteSecret(
scope: { actor: CURRENT_USER }
secret: { key: $key, value: $token }
) {
status
errors {
message
type
}
}
}
`;
const variables = {
key: "api_token",
token: newToken,
};
NerdGraphMutation.mutate({ mutation: mutation, variables: variables }).then(
(data) => {
if (data.data.nerdStorageVaultWriteSecret.status === "SUCCESS") {
this.setState({token: newToken})
}
}
);
}
}
render() {
return <div>
<Grid className="wrapper">
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_A_DESCRIPTION}
version="A"
/>
</GridItem>
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_B_DESCRIPTION}
version="B"
/>
</GridItem>
<GridItem columnSpan={12}><hr /></GridItem>
<GridItem columnSpan={12}><NewsletterSignups /></GridItem>
<GridItem columnSpan={6}><TotalSubscriptions /></GridItem>
<GridItem columnSpan={6}><TotalCancellations /></GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='a' accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='b' accountId={ACCOUNT_ID} />
</GridItem>
<ChartGroup>
<GridItem columnSpan={6}>
<VersionPageViews version='a' />
</GridItem>
<GridItem columnSpan={6}>
<VersionPageViews version='b' />
</GridItem>
</ChartGroup>
<GridItem columnSpan={12}>
<EndTestSection
accountId={ACCOUNT_ID}
versionADescription={VERSION_A_DESCRIPTION}
versionBDescription={VERSION_B_DESCRIPTION}
/>
</GridItem>
<GridItem columnSpan={12}>
<PastTests accountId={ACCOUNT_ID} />
</GridItem>
</Grid>
</div>
}
}
nerdlets/ab-test-nerdlet/index.js

When you call storeToken() with a new token value, your Nerdlet uses NerdGraph APIs to mutate NerdStorageVault data for the api_token key. If the request to NerdGraph is successful, storeToken() updates state.token so that the new token is locally accessible.

Unlike NerdStorage, which has query and mutation components for your convenience, NerdStorageVault has no components in the SDK. Instead, you have to use NerdGraphQuery and NerdGraphMutation to interact with it

Important

It's important to remember that NerdStorageVault is limited to the user scope. Any other users of your New Relic One application will have their own NerdStorageVault data. This means that even other users on your account will need to enter their token separately.

Query your API token

Step 1 of 2

First, create methods and state to show and hide your API token prompt:

import React from 'react';
import {
ChartGroup,
Grid,
GridItem,
NerdGraphMutation,
} from 'nr1';
import EndTestSection from './end-test';
import NewsletterSignups from './newsletter-signups';
import PastTests from './past-tests';
import TotalCancellations from './total-cancellations';
import TotalSubscriptions from './total-subscriptions';
import VersionDescription from './description';
import VersionPageViews from './page-views';
import VersionTotals from './totals';
const ACCOUNT_ID = 123456 // <YOUR NEW RELIC ACCOUNT ID>
const VERSION_A_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter"'
const VERSION_B_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter and get a free shirt!"'
export default class AbTestNerdletNerdlet extends React.Component {
constructor() {
super(...arguments);
this.state = {
hideTokenPrompt: true,
token: null,
}
this.storeToken = this.storeToken.bind(this);
this.showPrompt = this.showPrompt.bind(this);
this.hidePrompt = this.hidePrompt.bind(this);
}
storeToken(newToken) {
if (newToken != this.state.token) {
const mutation = `
mutation($key: String!, $token: SecureValue!) {
nerdStorageVaultWriteSecret(
scope: { actor: CURRENT_USER }
secret: { key: $key, value: $token }
) {
status
errors {
message
type
}
}
}
`;
const variables = {
key: "api_token",
token: newToken,
};
NerdGraphMutation.mutate({ mutation: mutation, variables: variables }).then(
(data) => {
if (data.data.nerdStorageVaultWriteSecret.status === "SUCCESS") {
this.setState({token: newToken})
}
}
);
}
}
showPrompt() {
this.setState({ hideTokenPrompt: false });
}
hidePrompt() {
this.setState({ hideTokenPrompt: true });
}
render() {
return <div>
<Grid className="wrapper">
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_A_DESCRIPTION}
version="A"
/>
</GridItem>
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_B_DESCRIPTION}
version="B"
/>
</GridItem>
<GridItem columnSpan={12}><hr /></GridItem>
<GridItem columnSpan={12}><NewsletterSignups /></GridItem>
<GridItem columnSpan={6}><TotalSubscriptions /></GridItem>
<GridItem columnSpan={6}><TotalCancellations /></GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='a' accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='b' accountId={ACCOUNT_ID} />
</GridItem>
<ChartGroup>
<GridItem columnSpan={6}>
<VersionPageViews version='a' />
</GridItem>
<GridItem columnSpan={6}>
<VersionPageViews version='b' />
</GridItem>
</ChartGroup>
<GridItem columnSpan={12}>
<EndTestSection
accountId={ACCOUNT_ID}
versionADescription={VERSION_A_DESCRIPTION}
versionBDescription={VERSION_B_DESCRIPTION}
/>
</GridItem>
<GridItem columnSpan={12}>
<PastTests accountId={ACCOUNT_ID} />
</GridItem>
</Grid>
</div>
}
}
nerdlets/ab-test-nerdlet/index.js

state.hideTokenPrompt determines whether or not the prompt is visible. Now, you need a mechanism for revealing the prompt, which is hidden by default.

Step 2 of 2

Query NerdStorageVault for your api_token:

import React from 'react';
import {
ChartGroup,
Grid,
GridItem,
NerdGraphMutation,
NerdGraphQuery,
} from 'nr1';
import EndTestSection from './end-test';
import NewsletterSignups from './newsletter-signups';
import PastTests from './past-tests';
import TotalCancellations from './total-cancellations';
import TotalSubscriptions from './total-subscriptions';
import VersionDescription from './description';
import VersionPageViews from './page-views';
import VersionTotals from './totals';
const ACCOUNT_ID = 123456 // <YOUR NEW RELIC ACCOUNT ID>
const VERSION_A_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter"'
const VERSION_B_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter and get a free shirt!"'
export default class AbTestNerdletNerdlet extends React.Component {
constructor() {
super(...arguments);
this.state = {
hideTokenPrompt: true,
token: null,
}
this.storeToken = this.storeToken.bind(this);
this.showPrompt = this.showPrompt.bind(this);
this.hidePrompt = this.hidePrompt.bind(this);
}
storeToken(newToken) {
if (newToken != this.state.token) {
const mutation = `
mutation($key: String!, $token: SecureValue!) {
nerdStorageVaultWriteSecret(
scope: { actor: CURRENT_USER }
secret: { key: $key, value: $token }
) {
status
errors {
message
type
}
}
}
`;
const variables = {
key: "api_token",
token: newToken,
};
NerdGraphMutation.mutate({ mutation: mutation, variables: variables }).then(
(data) => {
if (data.data.nerdStorageVaultWriteSecret.status === "SUCCESS") {
this.setState({token: newToken})
}
}
);
}
}
showPrompt() {
this.setState({ hideTokenPrompt: false });
}
hidePrompt() {
this.setState({ hideTokenPrompt: true });
}
componentDidMount() {
const query = `
query($key: String!) {
actor {
nerdStorageVault {
secret(key: $key) {
value
}
}
}
}
`;
const variables = {
key: "api_token",
};
NerdGraphQuery.query(
{
query: query,
variables: variables,
}
).then(
({ loading, error, data }) => {
if (error) {
console.error(error);
this.showPrompt();
}
if (data && data.actor.nerdStorageVault.secret) {
this.setState({ token: data.actor.nerdStorageVault.secret.value })
} else {
this.showPrompt();
}
}
)
}
render() {
return <div>
<Grid className="wrapper">
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_A_DESCRIPTION}
version="A"
/>
</GridItem>
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_B_DESCRIPTION}
version="B"
/>
</GridItem>
<GridItem columnSpan={12}><hr /></GridItem>
<GridItem columnSpan={12}><NewsletterSignups /></GridItem>
<GridItem columnSpan={6}><TotalSubscriptions /></GridItem>
<GridItem columnSpan={6}><TotalCancellations /></GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='a' accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='b' accountId={ACCOUNT_ID} />
</GridItem>
<ChartGroup>
<GridItem columnSpan={6}>
<VersionPageViews version='a' />
</GridItem>
<GridItem columnSpan={6}>
<VersionPageViews version='b' />
</GridItem>
</ChartGroup>
<GridItem columnSpan={12}>
<EndTestSection
accountId={ACCOUNT_ID}
versionADescription={VERSION_A_DESCRIPTION}
versionBDescription={VERSION_B_DESCRIPTION}
/>
</GridItem>
<GridItem columnSpan={12}>
<PastTests accountId={ACCOUNT_ID} />
</GridItem>
</Grid>
</div>
}
}
nerdlets/ab-test-nerdlet/index.js

Here, in componentDidMount(), you've queried NerdGraph for your api_token data. componentDidMount() is a React lifecycle method that's called when your component is mounted in the component tree. This means that early in it's setup process, your application will request your api_token.

If the NerdGraph query responds successfully with your token from NerdStorageVault, it sets the token in state. Otherwise, it shows the prompt so that you can enter a token.

This is great for storing an initial token, but what if you enter the wrong token or the API changes? You need a way reveal the prompt on-demand. Next, you'll create the actual token prompt and a button to manually invoke the prompt.

Create your token prompt

Step 1 of 9

In nerdlets/ab-test-nerdlet, add a new Javascript file named token-prompt.js:

bash
$
touch token-prompt.js
Step 2 of 9

In this new file, create a button that allows you to enter a new token on demand:

import React from 'react';
import { Button } from 'nr1';
class ApiTokenButton extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Button onClick={this.props.showPrompt}>Update API token</Button>
)
}
}
nerdlets/ab-test-nerdlet/token-prompt.js

ApiTokenButton receives showPrompt() in its props and calls that method when its Button is clicked.

Step 3 of 9

Create a token prompt using a Modal with a TextField:

import React from 'react';
import {
Button,
Modal,
TextField,
} from 'nr1';
class ApiTokenButton extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Button onClick={this.props.showPrompt}>Update API token</Button>
)
}
}
class ApiTokenPrompt extends React.Component {
constructor() {
super(...arguments);
this.state = {
token: null,
tokenError: false,
};
this.submitToken = this.submitToken.bind(this);
this.hideTokenError = this.hideTokenError.bind(this);
this.changeToken = this.changeToken.bind(this);
this.keyPress = this.keyPress.bind(this);
}
showTokenError() {
this.setState({ tokenError: true });
}
hideTokenError() {
this.setState({ tokenError: false });
}
changeToken(event) {
this.setState({ token: event.target.value });
}
submitToken(event) {
event.preventDefault();
if (this.state.token) {
this.props.storeToken(this.state.token)
this.hideTokenError()
this.props.hidePrompt()
} else {
this.showTokenError()
}
}
keyPress(event) {
if(event.keyCode == 13) {
event.preventDefault();
this.submitToken(event);
}
}
render() {
return <Modal hidden={this.props.hideTokenPrompt} onClose={this.props.hidePrompt}>
To see cancellation data, you need to enter an API token for your backend service:
<form>
<TextField label="API token" onChange={this.changeToken} onKeyDown={this.keyPress} invalid={this.state.tokenError ? "Token required" : false} />
<Button type={Button.TYPE.PRIMARY} onClick={this.submitToken}>Submit</Button>
</form>
</Modal>
}
}
nerdlets/ab-test-nerdlet/token-prompt.js

ApiTokenPrompt renders a Modal with a TextField, a Button, and an explanatory prompt. You use the Modal to enter your API token. It also provides basic error handling if you try to submit the form with no token value.

It's important to distinguish the token in AbTestNerdletNerdlet.state and the token in ApiTokenPrompt.state. The token in your Nerdlet's state is the current token that your Nerdlet knows about. It's this token that matches what's in NerdStorageVault. The token in ApiTokenPrompt.state is a fluid value that changes as you update the text in the TextField. When you press Submit in the modal, ApiTokenPrompt submits its token to your Nerdlet's storeToken() method. Then, storeToken() mutates NerdStorageVault with the new token.

You also implemented a few methods to improve the user experience:

  • keyPress() submits the token when the RETURN key is pressed
  • showTokenError() and hideTokenError() remind the user that they must enter a token before submitting the form
Step 4 of 9

Export your components, so you can use them in your Nerdlet:

import React from 'react';
import {
Button,
Modal,
TextField,
} from 'nr1';
class ApiTokenButton extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Button onClick={this.props.showPrompt}>Update API token</Button>
)
}
}
class ApiTokenPrompt extends React.Component {
constructor() {
super(...arguments);
this.state = {
token: null,
tokenError: false,
};
this.submitToken = this.submitToken.bind(this);
this.hideTokenError = this.hideTokenError.bind(this);
this.changeToken = this.changeToken.bind(this);
this.keyPress = this.keyPress.bind(this);
}
showTokenError() {
this.setState({ tokenError: true });
}
hideTokenError() {
this.setState({ tokenError: false });
}
changeToken(event) {
this.setState({ token: event.target.value });
}
submitToken(event) {
event.preventDefault();
if (this.state.token) {
this.props.storeToken(this.state.token)
this.hideTokenError()
this.props.hidePrompt()
} else {
this.showTokenError()
}
}
keyPress(event) {
if(event.keyCode == 13) {
event.preventDefault();
this.submitToken(event);
}
}
render() {
return <Modal hidden={this.props.hideTokenPrompt} onClose={this.props.hidePrompt}>
To see cancellation data, you need to enter an API token for your backend service:
<form>
<TextField label="API token" onChange={this.changeToken} onKeyDown={this.keyPress} invalid={this.state.tokenError ? "Token required" : false} />
<Button type={Button.TYPE.PRIMARY} onClick={this.submitToken}>Submit</Button>
</form>
</Modal>
}
}
export { ApiTokenButton, ApiTokenPrompt }
nerdlets/ab-test-nerdlet/token-prompt.js
Step 5 of 9

In your Nerdlet's index.js file, import ApiTokenButton and ApiTokenPrompt and add them to render():

import React from 'react';
import {
ChartGroup,
Grid,
GridItem,
NerdGraphMutation,
NerdGraphQuery,
} from 'nr1';
import EndTestSection from './end-test';
import NewsletterSignups from './newsletter-signups';
import PastTests from './past-tests';
import TotalCancellations from './total-cancellations';
import TotalSubscriptions from './total-subscriptions';
import VersionDescription from './description';
import VersionPageViews from './page-views';
import VersionTotals from './totals';
import { ApiTokenButton, ApiTokenPrompt } from './token-prompt';
const ACCOUNT_ID = 123456 // <YOUR NEW RELIC ACCOUNT ID>
const VERSION_A_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter"'
const VERSION_B_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter and get a free shirt!"'
export default class AbTestNerdletNerdlet extends React.Component {
constructor() {
super(...arguments);
this.state = {
hideTokenPrompt: true,
token: null,
}
this.storeToken = this.storeToken.bind(this);
this.showPrompt = this.showPrompt.bind(this);
this.hidePrompt = this.hidePrompt.bind(this);
}
storeToken(newToken) {
if (newToken != this.state.token) {
const mutation = `
mutation($key: String!, $token: SecureValue!) {
nerdStorageVaultWriteSecret(
scope: { actor: CURRENT_USER }
secret: { key: $key, value: $token }
) {
status
errors {
message
type
}
}
}
`;
const variables = {
key: "api_token",
token: newToken,
};
NerdGraphMutation.mutate({ mutation: mutation, variables: variables }).then(
(data) => {
if (data.data.nerdStorageVaultWriteSecret.status === "SUCCESS") {
this.setState({token: newToken})
}
}
);
}
}
showPrompt() {
this.setState({ hideTokenPrompt: false });
}
hidePrompt() {
this.setState({ hideTokenPrompt: true });
}
componentDidMount() {
const query = `
query($key: String!) {
actor {
nerdStorageVault {
secret(key: $key) {
value
}
}
}
}
`;
const variables = {
key: "api_token",
};
NerdGraphQuery.query(
{
query: query,
variables: variables,
}
).then(
({ loading, error, data }) => {
if (error) {
console.error(error);
this.showPrompt();
}
if (data && data.actor.nerdStorageVault.secret) {
this.setState({ token: data.actor.nerdStorageVault.secret.value })
} else {
this.showPrompt();
}
}
)
}
render() {
return <div>
<ApiTokenPrompt
hideTokenPrompt={this.state.hideTokenPrompt}
hidePrompt={this.hidePrompt}
showPrompt={this.showPrompt}
storeToken={this.storeToken}
/>
<Grid className="wrapper">
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_A_DESCRIPTION}
version="A"
/>
</GridItem>
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_B_DESCRIPTION}
version="B"
/>
</GridItem>
<GridItem columnSpan={12}><hr /></GridItem>
<GridItem columnSpan={12}><NewsletterSignups /></GridItem>
<GridItem columnSpan={6}><TotalSubscriptions /></GridItem>
<GridItem columnSpan={6}><TotalCancellations /></GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='a' accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='b' accountId={ACCOUNT_ID} />
</GridItem>
<ChartGroup>
<GridItem columnSpan={6}>
<VersionPageViews version='a' />
</GridItem>
<GridItem columnSpan={6}>
<VersionPageViews version='b' />
</GridItem>
</ChartGroup>
<GridItem columnSpan={12}>
<EndTestSection
accountId={ACCOUNT_ID}
versionADescription={VERSION_A_DESCRIPTION}
versionBDescription={VERSION_B_DESCRIPTION}
/>
</GridItem>
<GridItem columnSpan={12}>
<PastTests accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={12}>
<ApiTokenButton showPrompt={this.showPrompt} />
</GridItem>
</Grid>
</div>
}
}
nerdlets/ab-test-nerdlet/index.js
Step 6 of 9

Navigate to the root of your Nerdpack at nru-programmability-course/add-nerdstoragevault/ab-test.

Step 7 of 9

Generate a new UUID for your Nerdpack:

bash
$
nr1 nerdpack:uuid -gf

Because you cloned the coursework repository that contained an existing Nerdpack, you need to generate your own unique identifier. This UUID maps your Nerdpack to your New Relic account. It also allows your app to make Nerdgraph requests on behalf of your account.

Step 8 of 9
bash
$
nr1 nerdpack:serve
Step 9 of 9

Go to https://one.newrelic.com?nerdpacks=local, and view your application under Apps > Your apps:

API token prompt

When you visit your application for the first time, the prompt is automatically revealed. Enter "ABC123" in the TextField, as that's the token that the third-party service expects. Once you submit your token and your Nerdlet hides the prompt, click Update API token at the bottom of your New Relic One application to reveal it again:

Update API token button

Tip

If something doesn't work, use your browser's debug tools to try to identify the problem.

Make sure you:

  • Copied the code correctly from the lesson
  • Generated a new UUID
  • Replaced all instances of <YOUR NEW RELIC ACCOUNT ID> in your project with your actual New Relic account ID

Pass your API token to TotalCancellations

Step 1 of 3

In index.js, pass the API token to TotalCancellations so you're prepared to make a request to the third-party service:

import React from 'react';
import {
ChartGroup,
Grid,
GridItem,
NerdGraphMutation,
NerdGraphQuery,
} from 'nr1';
import EndTestSection from './end-test';
import NewsletterSignups from './newsletter-signups';
import PastTests from './past-tests';
import TotalCancellations from './total-cancellations';
import TotalSubscriptions from './total-subscriptions';
import VersionDescription from './description';
import VersionPageViews from './page-views';
import VersionTotals from './totals';
import { ApiTokenButton, ApiTokenPrompt } from './token-prompt';
const ACCOUNT_ID = 123456 // <YOUR NEW RELIC ACCOUNT ID>
const VERSION_A_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter"'
const VERSION_B_DESCRIPTION = 'The newsletter signup message says, "Sign up for our newsletter and get a free shirt!"'
export default class AbTestNerdletNerdlet extends React.Component {
constructor() {
super(...arguments);
this.state = {
hideTokenPrompt: true,
token: null,
}
this.storeToken = this.storeToken.bind(this);
this.showPrompt = this.showPrompt.bind(this);
this.hidePrompt = this.hidePrompt.bind(this);
}
storeToken(newToken) {
if (newToken != this.state.token) {
const mutation = `
mutation($key: String!, $token: SecureValue!) {
nerdStorageVaultWriteSecret(
scope: { actor: CURRENT_USER }
secret: { key: $key, value: $token }
) {
status
errors {
message
type
}
}
}
`;
const variables = {
key: "api_token",
token: newToken,
};
NerdGraphMutation.mutate({ mutation: mutation, variables: variables }).then(
(data) => {
if (data.data.nerdStorageVaultWriteSecret.status === "SUCCESS") {
this.setState({token: newToken})
}
}
);
}
}
showPrompt() {
this.setState({ hideTokenPrompt: false });
}
hidePrompt() {
this.setState({ hideTokenPrompt: true });
}
componentDidMount() {
const query = `
query($key: String!) {
actor {
nerdStorageVault {
secret(key: $key) {
value
}
}
}
}
`;
const variables = {
key: "api_token",
};
NerdGraphQuery.query(
{
query: query,
variables: variables,
}
).then(
({ loading, error, data }) => {
if (error) {
console.error(error);
this.showPrompt();
}
if (data && data.actor.nerdStorageVault.secret) {
this.setState({ token: data.actor.nerdStorageVault.secret.value })
} else {
this.showPrompt();
}
}
)
}
render() {
return <div>
<ApiTokenPrompt
hideTokenPrompt={this.state.hideTokenPrompt}
hidePrompt={this.hidePrompt}
showPrompt={this.showPrompt}
storeToken={this.storeToken}
/>
<Grid className="wrapper">
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_A_DESCRIPTION}
version="A"
/>
</GridItem>
<GridItem columnSpan={6}>
<VersionDescription
description={VERSION_B_DESCRIPTION}
version="B"
/>
</GridItem>
<GridItem columnSpan={12}><hr /></GridItem>
<GridItem columnSpan={12}><NewsletterSignups /></GridItem>
<GridItem columnSpan={6}><TotalSubscriptions /></GridItem>
<GridItem columnSpan={6}>
<TotalCancellations token={this.state.token} />
</GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='a' accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={6}>
<VersionTotals version='b' accountId={ACCOUNT_ID} />
</GridItem>
<ChartGroup>
<GridItem columnSpan={6}>
<VersionPageViews version='a' />
</GridItem>
<GridItem columnSpan={6}>
<VersionPageViews version='b' />
</GridItem>
</ChartGroup>
<GridItem columnSpan={12}>
<EndTestSection
accountId={ACCOUNT_ID}
versionADescription={VERSION_A_DESCRIPTION}
versionBDescription={VERSION_B_DESCRIPTION}
/>
</GridItem>
<GridItem columnSpan={12}>
<PastTests accountId={ACCOUNT_ID} />
</GridItem>
<GridItem columnSpan={12}>
<ApiTokenButton showPrompt={this.showPrompt} />
</GridItem>
</Grid>
</div>
}
}
nerdlets/ab-test-nerdlet/index.js
Step 2 of 3

In total-cancellations.js, log the token to your browser console:

import React from 'react';
import { HeadingText, PieChart } from 'nr1';
export default class TotalCancellations extends React.Component {
constructor() {
super(...arguments);
this.state = {
lastToken: null
}
}
componentDidUpdate() {
if (this.props.token && this.props.token != this.state.lastToken) {
console.log(`requesting data with api token ${this.props.token}`)
this.setState({lastToken: this.props.token})
}
}
render() {
const cancellationsA = {
metadata: {
id: 'cancellations-A',
name: 'Version A',
viz: 'main',
color: 'blue',
},
data: [
{ y: 118 },
],
}
const cancellationsB = {
metadata: {
id: 'cancellations-B',
name: 'Version B',
viz: 'main',
color: 'green',
},
data: [
{ y: 400 },
],
}
return <div>
<HeadingText className="chartHeader">
Total cancellations per version
</HeadingText>
<PieChart data={[cancellationsA, cancellationsB]} fullWidth />
</div>
}
}
nerdlets/ab-test-nerdlet/total-cancellations.js

Here, you've implemented another React lifecycle method, called componentDidUpdate(). Now, every time your Nerdlet's state.token changes, TotalCancellations gets a new token prop, which triggers componentDidUpdate(). In componentDidUpdate(), you check that the incoming token is not the same as the last token it knew about, which is stored in local state. If the incoming token is different, you log a message with the new token and update state.lastToken.

This logic prepares your code for a future change to use your API token in a request to a third-party service.

Step 3 of 3

With your Nerdpack served locally, view your application to see the log from TotalCancellations in your browser's console:

Total cancellations request log

If you change your token, you'll see another log from TotalCancellations with your updated token.

Tip

If something doesn't work, use your browser's debug tools to try to identify the problem.

Make sure you:

  • Copied the code correctly from the lesson
  • Generated a new UUID
  • Replaced all instances of <YOUR NEW RELIC ACCOUNT ID> in your project with your actual New Relic account ID

When you're finished, stop serving your New Relic One application by pressing CTRL+C in your local server's terminal window.

Now you know how to use NerdGraphQuery and NerdGraphMutation to manage data in NerdStorageVault! Remember, use NerdStorage for your New Relic One application's non-sensitive data and NerdStorageVault for the sensitive stuff like tokens, passwords, and other secrets. As a bonus, you created a way to manage your token in NerdStorageVault from the user interface. You also passed the token to your TotalCancellations component for later use.

Whether it's been with NrqlQuery, AccountStorageQuery, AccountStorageMutation, NerdGraphQuery, or NerdGraphMutation, you've learned several ways to interact with New Relic data in your New Relic One application. But New Relic One applications aren't just another way to show New Relic data. The purpose of New Relic One applications is to show you how your software is helping you meet your business objectives. Sometimes, New Relic data is all you need to make that happen, but other times you need to look beyond New Relic for data to fill in the gaps.