Skip to main content

Store app settings

This guide shows you how to create a settings schema and consume settings values in your UI.

Note

All authenticated users of the app can read app settings. Read more about the general concept of App settings.

Install the SDK

First, you need to install the required SDK via the terminal as follows:

npm i @dynatrace-sdk/client-app-settings

Define a settings schema

A settings schema is a JSON file that defines the structure of the values stored in the app settings. You can create any number of schemas in your project's /settings/schemas directory, with the only restriction that each schema definition file name must end in .schema.json.

Tip

If you want your IDE to provide autocompletion and detailed documentation, use the $schema keyword in your setting schema to reference the JSON Schema. You can also import it manually if your IDE doesn't support that mechanism.

Here is an example of a schema that allows you to configure connections to a fictional messaging service:

my-example.schema.json
settings/schemas/my-example.schema.json
{
"$schema": "https://developer.dynatrace.com/docs-assets/schema_strict_apps.json",
"dynatrace": "1",
"schemaId": "my-example",
"version": "1.0.0",
"displayName": "Allows you to configure connections to a fictional messaging service",
"description": "",
"multiObject": true,
"maxObjects": 10,
"summaryPattern": "Messaging Service {url} via {tokenType}",
"ordered": false,
"enums": {
"Type": {
"displayName": "Type",
"description": "",
"documentation": "",
"items": [
{
"value": "basic",
"displayName": "Basic Authentication"
},
{
"value": "pat",
"displayName": "Personal Access Token"
},
{
"value": "cloud-token",
"displayName": "Access Token"
}
],
"type": "enum"
}
},
"properties": {
"url": {
"displayName": "URL",
"description": "The URL of the messaging service",
"type": "text",
"default": "",
"nullable": false,
"constraints": [
{
"type": "PATTERN",
"customMessage": "The URL must be secure (https://) and must not contain a trailing slash",
"pattern": "^https://.*[^/]$"
}
]
},
"token": {
"displayName": "Token",
"description": "A secret token",
"type": "secret",
"default": "",
"nullable": false
},
"tokenType": {
"displayName": "Type",
"description": "Type of authentication method that should be used",
"default": "basic",
"type": {
"$ref": "#/enums/Type"
},
"nullable": false
},
"channels": {
"displayName": "Channels",
"description": "A list of channels on the messaging service",
"type": "list",
"items": {
"type": "text"
}
},
"isEnabled": {
"displayName": "Enabled",
"description": "Enable the integration of the messaging service",
"type": "boolean",
"default": false,
"nullable": false
}
}
}

Use secrets

When you use secrets in your app settings, a default security mechanism protects them from being exfiltrated.

For example, if you store an access token along with the URL for a service to authenticate, any URL change ideally requires you to resubmit the secret. Otherwise, your app could connect to the changed URL and leak the access token. You can prevent such a leak by resubmitting the secret after changing any properties related to it, such as URLs, IP addresses, port numbers, service names, and others.

To make this easy, app settings have a secret resubmission validation in place, which is achieved and configured with the help of a built-in container constraint.

Suppose the schema has secrets, and you haven't manually added a resubmission constraint. In this case, the schema will behave as if it had a constraint that requires you to resubmit the secret on any property change.

Therefore, by default, you'll have to resubmit the secret after any changes to its related properties. Add a constraint object to the list of constraints to change this default behavior. The constraint object has two mandatory fields, "type": "SECRET_RESUBMISSION", "checkAllProperties": false, and an optional one, "customMessage".

For every property related to the secret, add "forceSecretResubmission": true. The "forceSecretResubmission" property on an input property is only available if a validator constraint of type SECRET_RESUBMISSION exists. Otherwise, the schema registration will fail with a validation error, which means the automatism fallback always considers all properties and doesn't have to deal with the forceSecretResubmission properties.

Here is an example of a schema with a manually configured resubmission validation:

my-example-secrets.schema.json
settings/schemas/my-example-secrets.schema.json
{
"dynatrace": "1",
"schemaId": "my-example-secrets",
"version": "1.0.0",
"displayName": "Allows you to configure connections to a fictional messaging service",
"description": "",
"multiObject": true,
"maxObjects": 10,
"summaryPattern": "Messaging Service {url}",
"ordered": false,
"properties": {
"isEnabled": {
"displayName": "Enabled",
"description": "Enable the integration of the messaging service",
"type": "boolean",
"default": false,
"nullable": false
},
"description": {
"displayName": "Description of the connection",
"type": "text",
"default": "",
"nullable": false
},
"url": {
"displayName": "URL",
"description": "The URL of the messaging service",
"type": "text",
"default": "",
"nullable": false,
"forceSecretResubmission": true,
"constraints": [
{
"type": "PATTERN",
"customMessage": "The URL must be secure (https://) and must not contain a trailing slash",
"pattern": "^https://.*[^/]$"
}
]
},
"token": {
"displayName": "Token",
"description": "A secret token",
"type": "secret",
"default": "",
"nullable": false
}
},
"constraints": [
{
"type": "SECRET_RESUBMISSION",
"customMessage": "For security reasons, please re-enter the token before saving the settings.",
"checkAllProperties": false
}
]
}

Limitations

  • Each schema can't exceed 100KiB.
  • The settings folder can't exceed 1Mib.

Local development

Caution

The app settings plugin described in this chapter is still experimental and might change.

Although the autogenerated settings UI is available only after you deploy your app, you can mock the settings values locally for easier development. The app settings plugin allows you to provide, access, and update these mocked values during development.

First, install the plugin as a dev dependency:

npm install --save-dev @dynatrace-sdk/dt-app-plugin-client-app-settings

Next, register the plugin in your app configuration file, add the app-settings:objects:read app scope, and configure the file watcher:

{
"environmentUrl": "<Your-Environment-URL>",
"app": {
"id": "<Your-App-ID>",
"name": "<Your-App-Name>",
"version": "0.0.0",
"description": "<Your-App-Description>",
"scopes": [{ "name": "app-settings:objects:read", "comment": "Read app settings" }]
},
"plugins": ["@dynatrace-sdk/dt-app-plugin-client-app-settings"],
"dev": {
"fileWatcher": {
"ignore": ["**/{permissions,secrets,values,values.shadow}.json"]
}
}
}

After you start the development server, the app settings plugin creates some files:

  • settings/local-mock-data/values.json
  • settings/local-mock-data/secrets.json
  • settings/local-mock-data/permissions.json

Let's examine these files in the next sections.

Use local values

The settings/local-mock-data/values.json file allows you to provide the mocked values for your settings schema. When you fetch settings in your app locally, the plugin will handle the request and return the mocked values. Following is an example that shows all the properties you can set (schemaId and value are required; the rest is optional):

values.json
settings/local-mock-data/values.json
[
{
"schemaId": "my-example",
"value": {
"url": "https://foo.bar/messaging",
"token": "a-secret-basic-token",
"tokenType": "basic",
"channels": ["admin-notifications", "user-notifications"],
"isEnabled": true
},
"schemaVersion": "1",
"summary": "Messaging Service https://foo.bar/messaging via Basic Authentication",
"searchSummary": "Messaging Service https://foo.bar/messaging via Basic Authentication",
"modificationInfo": {
"createdBy": "foo",
"createdTime": "2023-10-10T10:00:00.000Z",
"lastModifiedBy": "bar",
"lastModifiedTime": "2023-11-11T11:00:00.000Z"
}
},
{
"schemaId": "my-example",
"value": {
"url": "https://foo.bar/messaging",
"token": "a-secret-personal-access-token",
"tokenType": "pat",
"channels": ["admin-notifications", "user-notifications"],
"isEnabled": true
},
"schemaVersion": "1",
"summary": "Messaging Service https://foo.bar/messaging via Personal Access Token",
"searchSummary": "Messaging Service https://foo.bar/messaging via Personal Access Token",
"modificationInfo": {
"createdBy": "foo",
"createdTime": "2023-08-08T08:00:00.000Z",
"lastModifiedBy": "baz",
"lastModifiedTime": "2023-09-09T09:00:00.000Z"
}
}
]

The above example has one issue: the access tokens are exposed as plain text. The app settings plugin also offers a solution to this problem.

Use local secrets

A common use case for app settings is storing secrets. The app settings plugin provides a way to simulate the same behavior locally by defining them in the settings/local-mock-data/secrets.json file. Following is an example:

settings/local-mock-data/secrets.json
{
"basicToken": "a-secret-basic-token",
"personalAccessToken": "a-secret-personal-access-token"
}

After the secrets are defined, you can now use them in the values.json file:

settings/local-mock-data/values.json
[
{
"schemaId": "my-example",
"value": {
"url": "https://foo.bar/messaging",
"token": "{{basicToken}}",
"tokenType": "basic",
"channels": ["admin-notifications", "user-notifications"],
"isEnabled": true
}
},
{
"schemaId": "my-example",
"value": {
"url": "https://foo.bar/messaging",
"token": "{{personalAccessToken}}",
"tokenType": "pat",
"channels": ["admin-notifications", "user-notifications"],
"isEnabled": true
}
}
]

If the settings are now read via App functions, the secrets are returned in plain text. However, if you read them elsewhere, they will be returned as a masked value, similar to when deploying the app.

Note, however, that when you update a value locally via the SDK, the secrets.json file isn't updated, and the property loses its secret attributes.

Use local permissions

The settings/local-mock-data/permissions.json file allows you to provide the mocked permissions for your settings schema. The plugin will use this to handle requests to resolve effective permissions and to fill the resourceContext field in the response to the fetch settings values request. Following is an example of defining permissions for settings schema with id "my-example":

{
"my-example": [
{
"permission": "app-settings:objects:read",
"granted": "true"
},
{
"permission": "app-settings:objects:write",
"granted": "false"
}
]
}

The plugin considers each of the two possible permissions (app-settings:objects:read, app-settings:objects:write) as granted for the current user unless the permissions.json file contains another value for the "granted" field for the given schema and permission.
For the resourceContext part of the settings values response, only "granted": "false" is treated as if the user wouldn't have the correspondent permission. Any other value (like "true", "condition" or missing value) is considered as "granted": "true". Also, note that the plugin doesn't use the permissions.json file to control user access while performing read or write operations with settings values.

Update local settings

Of course, you can update your values.json file at any point to update the settings for your app, but the plugin also supports modifications. For this purpose, when you execute one such operation, the plugin creates a copy of the values.json file in settings/local-mock-data/persistence/values.shadow.json. If this file exists, all read and write operations will use it instead of the settings/local-mock-data/values.json you created manually.

To restore the state you configured, simply delete the settings/local-mock-data/persistence folder.

Limitations

  • Local settings values aren't validated against the local schemas.
  • Intents to open settings pages don't work.

Get effective settings value

Effective values are the values that are stored in the settings or the default value. Use this method if you want to use settings in your app.

To retrieve the effective values for one or many settings schemas, use the useSettings React hook from the @dynatrace-sdk/react-hooks package. This hook expects a comma-separated string of schema identifiers as a parameter. A single call will get you the effective values for many schemas.

By default, useSettings only returns the value. The hook can return additional fields when you specify them in a comma-separated string in the addFields parameter.

In the following example, you're getting effective settings values for the my-example schemas:

import React from 'react';
import { useSettings } from '@dynatrace-sdk/react-hooks';
import { ExternalLink, Paragraph } from '@dynatrace/strato-components/typography';
import { Flex } from '@dynatrace/strato-components/layouts';

export const App = () => {
const { data, isLoading } = useSettings({
schemaIds: 'my-example',
addFields: 'summary, schemaId',
});

return (
<Flex>
{!isLoading && data && (
<>
<Paragraph>{data.items[0]?.summary}</Paragraph>
<ExternalLink href={data.items[0]?.value?.url}>My external link</ExternalLink>
</>
)}
</Flex>
);
};

The default value depends on the multiObject property of the schema:

  • false: if it's false, a single default object is created. Its properties are propagated with the default values defined in your schema.
  • true: if it's true, an empty collection is created.
Tip

This operation requires the scope app-settings:objects:read. Read more about scopes in this guide.

Store settings values

To store new values for a settings schema, use the useCreateSettings hook.

The useCreateSettings hook returns the objectId and version of the created settings object. You can use them to subsequently update or delete settings.

In the following example, you store a value accessible to the fictional messaging service integration from the my-example schema. The service can now use the specific URL and basic access token:

import React from 'react';
import { useCreateSettings } from '@dynatrace-sdk/react-hooks';
import { Button } from '@dynatrace/strato-components-preview/buttons';

export const App = () => {
const { execute } = useCreateSettings();

const handleOnClick = () => {
execute({
body: {
schemaId: 'my-example',
value: {
url: 'https://foo.bar/messaging',
token: 'a-secret-basic-token',
tokenType: 'basic',
channels: ['admin-notifications', 'user-notifications'],
isEnabled: true,
},
},
});
};

return <Button onClick={handleOnClick}>Create settings</Button>;
};
Tip

This operation requires the scope app-settings:objects:write. Read more about scopes in this guide.

Get stored settings values

To fetch previously stored values for one or many settings schemas, use the useSettingsObjects hook. Use this and the useUpdateSettings hook if you want to manage settings in your app.

This hook expects a comma-separated string of schema identifiers as a parameter, so getting the stored values for many schemas with a single call is possible.

import React from 'react';
import { useSettingsObjects } from '@dynatrace-sdk/react-hooks';
import { ExternalLink, Paragraph } from '@dynatrace/strato-components/typography';
import { Flex } from '@dynatrace/strato-components/layouts';

export const App = () => {
const { data, isLoading } = useSettingsObjects({
schemaIds: 'my-example',
addFields: 'value, summary',
});

return (
<Flex>
{!isLoading && data && (
<>
<Paragraph>{data.items[0]?.summary}</Paragraph>
<ExternalLink href={data.items[0]?.value?.url}>My external url</ExternalLink>
</>
)}
</Flex>
);
};

By default, useSettingsObjects only returns the objectId and version. You can define extra fields to be returned by specifying them in a comma-separated string in the addFields parameter.

Tip

This operation requires the scope app-settings:objects:read. Read more about scopes in this guide.

Update settings values

To update the value for a settings schema, use the useUpdateSettings hook. In the following example, you edit a value accessible to the fictional messaging service integration from the my-example schema. The service can now use the personal access token:

import React from 'react';
import { useSettingsObjects, useUpdateSettings } from '@dynatrace-sdk/react-hooks';
import { Button } from '@dynatrace/strato-components-preview/buttons';

export const App = () => {
const { data } = useSettingsObjects({
schemaIds: 'my-example',
});
const { execute } = useUpdateSettings();

const handleOnClick = () => {
if (data) {
execute({
objectId: data.items[0].objectId,
optimisticLockingVersion: data.items[0].version,
body: {
value: {
url: 'https://foo.bar/messaging',
token: 'a-secret-personal-access-token',
tokenType: 'pat',
channels: ['admin-notifications', 'user-notifications'],
isEnabled: true,
},
},
});
}
};

return <Button onClick={handleOnClick}>Update settings</Button>;
};

Remember that each setting has a version that changes each time its value changes. You need to pass the current version for any operation that modifies the value.

Tip

This operation requires the scope app-settings:objects:write. Read more about scopes in this guide.

Delete settings values

To delete a value for a settings schema, use the useDeleteSettings hook. You need to provide the optimisticLockingVersion of the value you want to delete, as shown in the following example:

import React from 'react';
import { useSettingsObjects, useDeleteSettings } from '@dynatrace-sdk/react-hooks';
import { Button } from '@dynatrace/strato-components-preview/buttons';

export const App = () => {
const { data } = useSettingsObjects({
schemaIds: 'my-example',
});
const { execute } = useDeleteSettings();

const handleOnClick = () => {
if (data) {
execute({
objectId: data.items[0].objectId,
optimisticLockingVersion: data.items[0].version,
});
}
};

return <Button onClick={handleOnClick}>Delete settings</Button>;
};
Tip

This operation requires the scope app-settings:objects:write. Read more about scopes in this guide.

Note

To use await in React components, you need to wrap the asynchronous invocation in an async function. Read more about it in this guide.

Get effective settings permissions

To get the effective settings permissions for the calling user in the environment, use the useEffectivePermissions hook. In the following example, you're checking whether the user has app-settings:objects:read and app-settings:objects:write permissions for the my-example schema:

import React from 'react';
import { Container, Flex, Text } from '@dynatrace/strato-components';
import { BlockIcon } from '@dynatrace/strato-icons';
import { useEffectivePermissions } from '@dynatrace-sdk/react-hooks';

export const App = () => {
const { data } = useEffectivePermissions({
body: {
permissions: [
{
permission: 'app-settings:objects:read',
context: { schemaId: 'my-example' },
},
{
permission: 'app-settings:objects:write',
context: { schemaId: 'my-example' },
},
],
},
});

return (
<Container>
{data && data[0].granted === 'false' && (
<Flex>
<BlockIcon />
<Text>Permission denied. Contact your account admin.</Text>
</Flex>
)}
</Container>
);
};

The function returns an array containing information about requested permissions in the following format:

[
{
"permission": "app-settings:objects:read",
"granted": "true"
},
{
"permission": "app-settings:objects:write",
"granted": "false"
}
]
Tip

This operation requires one of the following scopes:

  • app-settings:objects:read
  • app-settings:objects:write
Read more about scopes in this guide.

Note

To use await in React components, you need to wrap the asynchronous invocation in an async function. Read more about it in this guide.

Still have questions?
Find answers in the Dynatrace Community