Store app settings
This guide shows you how to create a settings schema and consume settings values in your UI.
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
.
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
{
"$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
{
"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
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:
- app.config.json
- app.config.ts
{
"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"]
}
}
}
import type { CliOptions } from 'dt-app';
const config: CliOptions = {
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']
}
}
}
module.exports = config;
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
[
{
"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:
{
"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:
[
{
"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.
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>;
};
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.
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.
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>;
};
This operation requires the scope app-settings:objects:write
. Read more about scopes in this guide.
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"
}
]
This operation requires one of the following scopes:
app-settings:objects:read
app-settings:objects:write
To use await
in React components, you need to wrap the asynchronous invocation in an async
function. Read more about it in this guide.