Skip to content

Upgrading from v0.54.0 to v0.55.0#

This document provides instructions for upgrading Perses specially when dealing with breaking changes. As Perses is a rapidly evolving project, it's important to keep your installation up to date to benefit from the latest features and improvements.

Then, upgrading Perses is also depending on your usage as Perses can be used in different ways (e.g. as a standalone application, as a library, etc.). Therefore, the upgrade process may vary based on your specific use case.

Perses application#

Resource names are validated more strictly#

The names of the resources (metadata.name and metadata.project), the panel keys and the variable names can no longer contain .., nor start or end with . (e.g. .foo, foo. or foo..bar). This validation is applied when a resource is saved, but also when it is read from the database and on every parameter of the API URLs.

A resource stored with such a name can no longer be read, updated or deleted once Perses is upgraded. Rename these resources before upgrading, while still running v0.54: re-create them with a valid name (e.g. with percli apply), then delete the old ones. Don't forget to update the references to the renamed panels ($ref in the layouts) and variables ($var).

You can find them with a command like this one, for each kind of resource (projects, dashboards, datasources, variables, secrets, roles, globaldatasources, ...):

curl -s "$PERSES_URL/api/v1/dashboards" | jq -r '.[] | select(.metadata.name | test("\\.\\.|^\\.|\\.$")) | "\(.metadata.project)/\(.metadata.name)"'

Important dashboards are now grouped#

frontend.important_dashboards is now a list of groups, each with an optional title and description, and a list of dashboard selectors. A selector without dashboard marks every dashboard of the project as important.

The previous flat list of selectors is deprecated and will be removed in v0.57.0. It is still accepted (and converted into a single untitled group), but a deprecation warning is logged at startup. Mixing both formats is rejected.

# Before
frontend:
  important_dashboards:
    - project: "perses"
      dashboard: "Demo"

# After
frontend:
  important_dashboards:
    - title: "Quick links" # Optional
      dashboards:
        - project: "perses"
          dashboard: "Demo"

If you set important dashboards through environment variables, the legacy shape is not supported there: Perses will fail to start until the variables are updated accordingly:

PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_PROJECT   -> PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_DASHBOARDS_0_PROJECT
PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_DASHBOARD -> PERSES_FRONTEND_IMPORTANT_DASHBOARDS_0_DASHBOARDS_0_DASHBOARD

Perses also refuses to start when a group has no dashboards, or when a selector has no project.

Note that the endpoint GET /api/config always returns frontend.important_dashboards with the new grouped shape, even when the legacy format is used in the configuration. Update the tools reading this endpoint accordingly.

The datasource proxy restricts the destinations it can reach#

To prevent the datasource proxy from being used to reach services that are not datasources (Server-Side Request Forgery), the destinations of the proxy are now verified. By default, the proxy refuses to reach:

  • the loopback interface (127.0.0.0/8, ::1, localhost)
  • the link-local addresses (169.254.0.0/16, fe80::/10) and the known cloud metadata / credentials endpoints
  • the multicast, reserved and unspecified addresses
  • the Kubernetes API service when Perses is running in a Kubernetes cluster
  • any URL scheme other than http and https, the URLs containing credentials (http://user:password@host), and the Unix sockets for the SQL datasources
  • the IPv4 addresses not written in the dotted-decimal notation (e.g. 2130706433, 0x7f000001 or 127.1)

The private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ...) remain allowed by default.

If one of your datasources is running on the same host as Perses (e.g. http://localhost:9090), the requests to this datasource are now refused with a 403 Forbidden error, and saving such a datasource is refused with a 400 Bad Request error. The same applies to the datasources embedded in a dashboard or an ephemeral dashboard (spec.datasources), and to the validation endpoints (POST /api/v1/validate/datasources and POST /api/v1/validate/globaldatasources). Provisioning a resource using such a datasource fails as well: the resource is skipped, and only an error is logged. You have to explicitly allow the loopback interface:

datasource:
  proxy:
    allowed_networks:
      - "127.0.0.0/8"
      - "::1/128"

The same applies to the OAuth token URL of the secrets used by the datasources. Saving a secret whose OAuth token URL is not allowed is refused with a 400 Bad Request error, and the requests to a datasource using such a secret are refused with a 403 Forbidden error. If you restrict the destinations with allowed_hosts, this list must also contain the host of the OAuth token URL.

When an IP address is part of both an allowed and a denied network, the most specific network wins (on equal prefix lengths, the allowed network wins). Allowing a large network therefore doesn't allow a more specific denied network, which has to be allowed explicitly. For example:

  • allowing 127.0.0.0/8 allows the loopback interface;
  • allowing 10.0.0.0/8 (e.g. with deny_private_networks: true) doesn't allow the Kubernetes API service IP;
  • allowing 169.254.0.0/16 doesn't allow the cloud metadata endpoint 169.254.169.254.

If Perses is using an HTTP proxy configured through the environment (HTTP_PROXY, HTTPS_PROXY), a datasource can no longer target the address of this proxy, neither directly (for example when the proxy is bypassed with NO_PROXY) nor through the proxy itself.

You can also further restrict the destinations, for example to deny the private networks or to only allow a list of hosts. See the DatasourceProxy config for more details.

Note for the users of v0.55.0-beta.4: the configuration of the HTTP proxy introduced in this beta (datasource.http_proxy) has moved to datasource.proxy.http, so the whole configuration of the datasource proxy lives in a single section.

# Before (v0.55.0-beta.4)
datasource:
  http_proxy:
    max_timeout: 30s

# After
datasource:
  proxy:
    http:
      max_timeout: 30s

The datasource proxy no longer forwards the credentials used to authenticate against Perses#

Before forwarding a request to a datasource, the proxy now removes the credentials that the client used to authenticate against Perses:

  • The Cookie header sent by the client is always removed, because it contains the Perses session (and possibly the tokens of the OIDC/OAuth provider). You can't forward it with allowHeaders. A Cookie header explicitly defined in the datasource configuration is still sent.
  • When the Perses native authorization is enabled, the Authorization header sent by the client (which contains the Perses token) is always removed. You can't forward it with allowHeaders. Use a Secret or the oauthPassthrough option to authenticate against the datasource.
  • When the authorization is delegated (i.e. Kubernetes), the Authorization header sent by the client is still forwarded to the datasource. To prevent this, add Authorization to dropHeaders.
  • An Authorization header defined in the headers of the datasource configuration is now always ignored, whatever its case (e.g. authorization). If you were using it to authenticate against the datasource, move the credentials to a Secret (authorization, basicAuth or oauth) and reference it in the datasource configuration:
# Before
kind: Datasource
spec:
  plugin:
    kind: PrometheusDatasource
    spec:
      proxy:
        kind: HTTPProxy
        spec:
          url: https://prometheus.example.com
          headers:
            Authorization: "Bearer <token>"

# After
kind: Secret
metadata:
  name: prometheus-token
  project: my-project
spec:
  authorization:
    type: Bearer
    credentials: "<token>"
---
kind: Datasource
spec:
  plugin:
    kind: PrometheusDatasource
    spec:
      proxy:
        kind: HTTPProxy
        spec:
          url: https://prometheus.example.com
          secret: prometheus-token

The responses returned by the datasources through the proxy are also hardened, as they are served under the Perses origin:

  • they get the headers Content-Security-Policy: sandbox; default-src 'none'; frame-ancestors 'none' and X-Content-Type-Options: nosniff;
  • the headers that would apply to the Perses origin are removed: Set-Cookie, Set-Cookie2, Clear-Site-Data, Refresh, Strict-Transport-Security, Alt-Svc, Service-Worker-Allowed and the CORS headers (the CORS policy is defined by security.cors);
  • only the relative Location / Content-Location headers are kept.

As a consequence, the web UI of a datasource (e.g. the Prometheus UI) can no longer be browsed through the proxy. Access it directly instead.

See the proxy documentation for more details.

Saving a dashboard embedding a datasource with a secret requires to read the secrets#

A datasource embedded in a dashboard or an ephemeral dashboard (spec.datasources) can reference a secret of the project. Creating or updating such a dashboard now requires the read permission on the Secret kind in the project. Otherwise, the request is refused with a 403 Forbidden error. This doesn't apply to the provisioning.

If your users need to save such dashboards, grant them the permission to read the secrets:

kind: Role
metadata:
  name: dashboard-editor
  project: my-project
spec:
  permissions:
    - actions: ["create", "update"]
      scopes: ["Dashboard"]
    - actions: ["read"]
      scopes: ["Secret"]

A secret can only define one authentication method#

basicAuth, authorization and oauth are now mutually exclusive in a Secret or a GlobalSecret. Previously, a secret was only refused when the three were set at the same time. A secret defining two of them is now refused when it is saved, and it can no longer be read once Perses is upgraded (the datasources using it then fail).

Before upgrading, keep only the authentication method actually used by the datasource:

# Before
kind: Secret
spec:
  basicAuth:
    username: "foo"
    password: "bar"
  authorization:
    type: Bearer
    credentials: "<token>"

# After
kind: Secret
spec:
  authorization:
    type: Bearer
    credentials: "<token>"

The SQL proxy only accepts read-only queries#

To prevent a datasource from being used to modify the data or the schema of a database, the SQL proxy is now stricter:

  • A query must start with one of these keywords: SELECT, WITH, SHOW, DESCRIBE, DESC, EXPLAIN, VALUES or TABLE. Previously, only the queries starting with a known write keyword (INSERT, UPDATE, DELETE, DROP, ...) were refused. The queries starting with another keyword (e.g. CALL, SET, DO, PRAGMA) are now refused.
  • Every query is executed in a read-only transaction. The database must support read-only transactions.
  • The queries using INTO OUTFILE or INTO DUMPFILE (MySQL / MariaDB) are refused.
  • For MySQL / MariaDB, the driver options multiStatements, allowAllFiles, allowCleartextPasswords and allowOldPasswords are always disabled, whatever the value set in the datasource. If a database user authenticates with a cleartext password (e.g. mysql_clear_password with PAM or LDAP), switch it to caching_sha2_password.
  • For PostgreSQL, the option prepareThreshold is ignored.

Rewrite the queries that don't start with one of the allowed keywords, and split the queries containing several statements.

Variables referenced with ${var} are now validated#

The variables referenced in other variables are now detected with both syntaxes $var and ${var}. Previously, a variable referencing an undefined variable, or a circular dependency between variables, was silently accepted when using the syntax ${var}. Saving such a dashboard (or ephemeral dashboard) is now refused with an error like variable "x" is used in the variable "y" but not defined.

Define the missing variables or remove the references. You can check your dashboards before applying them with percli lint -f <file>.

Pyroscope datasource: the profile queries use new endpoints#

The Pyroscope plugin (v0.7.0) now queries the profiles through the Connect API of Pyroscope instead of GET /pyroscope/render. If your Pyroscope datasources restrict the endpoints reachable through the proxy (allowedEndpoints), the profile queries are refused with a 403 Forbidden error until the new endpoints are allowed:

kind: Datasource
spec:
  plugin:
    kind: PyroscopeDatasource
    spec:
      proxy:
        kind: HTTPProxy
        spec:
          url: http://pyroscope:4040
          allowedEndpoints:
            # Before
            # - endpointPattern: /pyroscope/render
            #   method: GET
            # After
            - endpointPattern: /querier.v1.QuerierService/SelectMergeStacktraces
              method: POST
            - endpointPattern: /querier.v1.QuerierService/SelectSeries
              method: POST
            - endpointPattern: /querier.v1.QuerierService/ProfileTypes
              method: POST
            - endpointPattern: /querier.v1.QuerierService/LabelNames
              method: POST
            - endpointPattern: /querier.v1.QuerierService/LabelValues
              method: POST

Files referenced in secrets must be in an allowed directory#

The Secrets and GlobalSecrets can reference files from the Perses server (basicAuth.passwordFile, authorization.credentialsFile, oauth.clientSecretFile, tlsConfig.caFile, tlsConfig.certFile, tlsConfig.keyFile). This could be used by anyone able to create a secret to read arbitrary files through the datasource proxy.

These file references are now rejected by default. If you rely on them, you must list the directories where the files are located with the new setting security.secret_file_allowed_directories:

security:
  secret_file_allowed_directories:
    - /etc/perses/secrets

Each directory must be an absolute path, and the root directory (/) is not accepted. The file paths used in the secrets must also be absolute. Symlinks are resolved, and the resolved file must remain inside one of the allowed directories. The verification is done when saving a secret and before the proxy uses it. Saving a secret referencing a file that is not allowed is refused, and the requests to a datasource using such a secret fail.

encryption_key is mandatory when the authentication is enabled#

When the authentication is enabled (security.enable_auth: true), the encryption key is also used to encrypt the access and refresh tokens. Perses was previously falling back on a default (and publicly known) key when none was provided.

Now, if the authentication is enabled and neither security.encryption_key nor security.encryption_key_file is set, Perses refuses to start. The key must be exactly 32 bytes long. You can generate one with one of these commands:

LC_ALL=C tr -dc '[:graph:]' < /dev/urandom | head -c 32
openssl rand -base64 24 | head -c 32
security:
  enable_auth: true
  encryption_key: "<32 bytes long secret>"
  # or
  encryption_key_file: /etc/perses/encryption_key

Note that if you were already running Perses with the auth enabled and without any encryption key, setting a new key means the data previously encrypted with the default key (e.g. the secrets stored in the database) can no longer be decrypted. You will have to re-create these secrets after the upgrade, and users will need to log in again.

Plugin developer#

Go and CUE toolchains#

  • The Go modules (github.com/perses/perses including the Go SDK, github.com/perses/spec and the official plugins) now require Go 1.27.2 or later. Upgrade Go, or let Go download the right toolchain (GOTOOLCHAIN=auto), and bump the go directive of your go.mod if needed.
  • The CUE modules of Perses and of the official plugins now declare the CUE language version v0.17.1. The cue CLI must be at least v0.17.1, otherwise it fails with an error like language version "v0.17.1" declared in module.cue is too new. percli must also be at least v0.55.0 to build Dashboard-as-Code depending on these modules.

Go SDK: datasource.Selector is now a union type#

datasource.Selector has been restructured as a union type to make the distinction between a concrete datasource reference and a variable reference explicit. The flat struct (with public Kind and Name fields) no longer exists.

Before:

// Concrete datasource — struct literal
sel := &datasource.Selector{Kind: "PrometheusDatasource", Name: "myPrometheus"}

// Reading fields
fmt.Println(sel.Kind, sel.Name)

After:

// Concrete datasource — use the constructor
sel := datasource.NewStaticSelector("PrometheusDatasource", "myPrometheus")

// Variable reference — new in this release; name may be "foo", "$foo", or "${foo}"
sel, err := datasource.NewVariableSelector("myDatasource")

// Reading fields — check which variant is set first
if sel.Static != nil {
    fmt.Println(sel.Static.Kind, sel.Static.Name)
}
if sel.Variable != nil {
    fmt.Println(sel.Variable.Name) // bare name without "$"
}

If you maintain a datasource plugin with a Selector helper, update it to call the constructors. This is what the official plugins now do: their Selector helper returns an error as well, and returns a variable selector when the name starts with $ (e.g. prometheus.Selector("$ds")):

// Before
func Selector(datasourceName string) *datasource.Selector {
    return &datasource.Selector{
        Kind: PluginKind,
        Name: datasourceName,
    }
}

// After
func Selector(datasourceName string) (*datasource.Selector, error) {
    return datasource.NewSelector(PluginKind, datasourceName)
}

If you use the Selector helper of an official plugin (Prometheus, Tempo, Loki, ...), handle the returned error:

// Before
sel := prometheus.Selector("myPrometheus")

// After
sel, err := prometheus.Selector("myPrometheus")
if err != nil {
    return err
}

A static selector continues to serialize as {"kind":"...","name":"..."} (unchanged from previous versions). A variable selector serializes as the plain JSON/YAML string "$myVar". This form was already accepted by the CUE schemas and the UI, but it can now also be produced with the Go SDK. As a consequence, a datasource name starting with $ passed to a Selector helper is now serialized as "$myVar" instead of {"kind":"...","name":"$myVar"}. Note that marshaling a zero-value Selector (neither Static nor Variable set) or one with both fields set returns an error.

If you don't know in advance whether the name is a variable reference or not, use datasource.NewSelector(kind, name): it returns a variable selector when name starts with $, and a static selector otherwise.

Go: the old dashboard and datasource specification is removed#

Since v0.54.0, the dashboard and datasource specification lives in the repository perses/spec. The deprecated copies kept in perses/perses are now removed. If you still import them, replace the import paths as follows:

Removed package (github.com/perses/perses/...) Replacement (github.com/perses/spec/...)
pkg/model/api/v1/common go/common (Plugin is now in go/plugin)
pkg/model/api/v1/dashboard go/dashboard
pkg/model/api/v1/variable go/dashboard/variable
pkg/model/api/v1/plugin go/plugin
pkg/model/api/v1/datasource/http go/datasource/proxy/http
pkg/model/api/v1/datasource/sql go/datasource/proxy/sql

The deprecated types in github.com/perses/perses/pkg/model/api/v1 (DashboardSpec, Panel, PanelSpec, PanelDisplay, Query, QuerySpec, Link, DatasourceSpec, ...) are also removed. Use their equivalent from github.com/perses/spec/go/dashboard and github.com/perses/spec/go/datasource (e.g. dashboard.Spec, dashboard.Panel, datasource.Spec).

Go: other API changes#

  • K8sAuth.GetToken() (package github.com/perses/perses/pkg/client/config) is removed, as the token is now provided by the Kubernetes client (including the exec credential plugins of the kubeconfig). Use WrapRoundTripper instead:
// Before
token, err := k8sAuth.GetToken()

// After
roundTripper, err := k8sAuth.WrapRoundTripper(http.DefaultTransport)
  • In github.com/perses/perses/pkg/model/api/config, Frontend.ImportantDashboards is now of type config.ImportantDashboards (a list of groups), see Important dashboards are now grouped.
  • The field hidden of the variable display (github.com/perses/spec/go/dashboard/variable.Display) is now omitted when it's false. If you compare the output of your Dashboard-as-Code with golden files, regenerate them.

CUE: the old schemas are removed#

For the same reason, the following CUE packages are removed:

Removed package Replacement
github.com/perses/perses/cue/model/api/v1/common github.com/perses/spec/cue/common (#Plugin is in github.com/perses/spec/cue/plugin)
github.com/perses/perses/cue/model/api/v1/dashboard github.com/perses/spec/cue/dashboard
github.com/perses/perses/cue/model/api/v1/variable github.com/perses/spec/cue/dashboard/variable
github.com/perses/perses/cue/model/api/v1/plugin github.com/perses/spec/cue/plugin
github.com/perses/perses/cue/model/api/v1/datasource/* github.com/perses/spec/cue/datasource/proxy/http and .../proxy/sql
github.com/perses/shared/cue/common/proxy github.com/perses/spec/cue/datasource/proxy/http and .../proxy/sql

v1.#DashboardSpec, v1.#Panel, v1.#PanelSpec, v1.#PanelDisplay, v1.#Query, v1.#QuerySpec and v1.#Link are replaced by dashboard.#Spec, dashboard.#Panel, dashboard.#PanelSpec, dashboard.#PanelDisplay, dashboard.#Query, dashboard.#QuerySpec and dashboard.#Link (from github.com/perses/spec/cue/dashboard). v1.#DatasourceSpec is replaced by datasource.#Spec (from github.com/perses/spec/cue/datasource).

If your plugin schemas were using the deprecated github.com/perses/shared/cue/common/proxy package, replace its definitions as follows:

Removed definition Replacement Package
proxy.#HTTPProxy http.#Proxy github.com/perses/spec/cue/datasource/proxy/http
proxy.#HTTPAllowedEndpoint http.#AllowedEndpoint github.com/perses/spec/cue/datasource/proxy/http
proxy.#baseHTTPDatasourceSpec datasource.#HTTPDatasourceSpec github.com/perses/spec/cue/datasource
proxy.#SQLProxy sql.#Proxy github.com/perses/spec/cue/datasource/proxy/sql
proxy.#MySQL sql.#MySQLConfig github.com/perses/spec/cue/datasource/proxy/sql
proxy.#Postgres sql.#PostgresConfig github.com/perses/spec/cue/datasource/proxy/sql
proxy.#baseSQLDatasourceSpec datasource.#SQLDatasourceSpec github.com/perses/spec/cue/datasource
// Before
import "github.com/perses/shared/cue/common/proxy"

spec: proxy.#baseHTTPDatasourceSpec

// After
import "github.com/perses/spec/cue/datasource"

spec: datasource.#HTTPDatasourceSpec

Don't forget to bump the dependencies in your cue.mod/module.cue (github.com/perses/perses/cue@v0 and github.com/perses/shared/cue@v0 to v0.55.x, github.com/perses/spec/cue@v0 to v0.3.x). Note that github.com/perses/shared/cue no longer depends on github.com/perses/spec/cue: if your module imports the spec, it must declare this dependency itself. Running cue mod tidy adds the missing dependencies.

As mentioned in Go and CUE toolchains, the cue CLI must be at least v0.17.1.

@perses-dev/core is removed#

As announced in v0.54.0, the package @perses-dev/core is no longer published, and the other packages don't depend on it anymore. If you are still importing it, follow the migration described in Core package deprecated: its content is now available in @perses-dev/spec, @perses-dev/client, @perses-dev/components, @perses-dev/plugin-system and @perses-dev/dashboards.

Toolchain and dependencies upgrade#

The Perses npm packages (@perses-dev/spec, @perses-dev/client, @perses-dev/components, @perses-dev/plugin-system, @perses-dev/dashboards, @perses-dev/explore), as well as the packages of the official plugins (@perses-dev/*-plugin), changed their requirements:

  • Node.js 24 (LTS) and npm 11 are now required to build the Perses projects (Perses itself, perses/shared and the plugins), including a plugin generated with percli plugin generate ("engines": { "node": ">=24", "npm": ">=11" }).
  • ESM only: the CommonJS build (dist/cjs and lib/cjs for the plugins) is no longer published. The packages are declared with "type": "module", and main and module both point to the ES module output. Projects consuming these packages must support ES modules (e.g. no require('@perses-dev/...')). The TypeScript projects consuming them must use "moduleResolution": "bundler" (node16 and nodenext can't resolve the published type declarations).
  • ES2023: the published code targets ES2023 (e.g. it uses Array.prototype.toSorted), so it requires a recent browser (Chrome / Edge 110+, Firefox 115+, Safari 16+) and Node.js 20+ for your tests.
  • React 17 is no longer supported: the peer dependencies are now react and react-dom ^18.3.0. @perses-dev/client now also has react (^18.3.0) as peer dependency.
  • Zod 4: zod is upgraded from ^3.25 to ^4 (including in @perses-dev/spec). The Zod schemas exported by the packages are typed with both their input and output types: z.ZodType<T> becomes z.ZodType<T, T> (e.g. datasourcesSchema, roleSpecSchema, userSchema, panelSpecSchema, datasourceSpecSchema...). They can't be composed with Zod 3 schemas. Follow the Zod 4 migration guide for your own schemas.
  • react-hook-form is upgraded to ^7.87.0 and @hookform/resolvers to ^5.9.1.
  • @perses-dev/spec must be upgraded to ^0.3.0.
  • The code is compiled with TypeScript 7.

The plugins built for v0.54 must be rebuilt against the v0.55 packages: Perses no longer shares @perses-dev/core with the plugins at runtime, and the shared packages no longer provide the APIs removed in this release.

If you are maintaining a plugin, the easiest way to align your project is to compare it with a project freshly generated with percli plugin generate. The main changes are:

  • in package.json:
    • add "type": "module" and the engines field;
    • remove the build:cjs script and the file .cjs.swcrc;
    • set "main": "lib/index.js";
    • upgrade the peer dependencies (@perses-dev/* to ^0.55.0, @perses-dev/spec to ^0.3.0, react / react-dom to ^18.3.0, react-hook-form to ^7.87.0, @hookform/resolvers to ^5.9.1);
  • in .swcrc, target es2023;
  • in tsconfig.json, target es2023 and use "moduleResolution": "bundler";
  • in rsbuild.config.ts, add @perses-dev/client to the shared singletons of the Module Federation configuration:
pluginModuleFederation({
  // ...
  shared: {
    // ...
    '@perses-dev/client': { singleton: true },
    '@perses-dev/components': { singleton: true },
    '@perses-dev/plugin-system': { singleton: true },
    '@perses-dev/explore': { singleton: true },
    '@perses-dev/dashboards': { singleton: true },
  },
});

Embedding Perses: the host must register its shared modules#

The plugin runtime (Module Federation) is now ESM-native: the Perses packages are no longer loaded with require when a remote plugin asks for them. Instead, the application hosting the plugins must provide its own instance of the Perses packages before any plugin is loaded, using registerHostSharedModules. Otherwise, loading a remote plugin fails with the error Shared module "..." was not registered before a plugin tried to consume it.

// Before
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
// After
import * as PersesClient from '@perses-dev/client';
import * as PersesComponents from '@perses-dev/components';
import * as PersesDashboards from '@perses-dev/dashboards';
import * as PersesExplore from '@perses-dev/explore';
import * as PersesPluginSystem from '@perses-dev/plugin-system';
import { registerHostSharedModules } from '@perses-dev/plugin-system';
import * as PersesSpec from '@perses-dev/spec';

// Provide the host's already-loaded Perses packages to the plugin runtime as Module Federation singletons.
registerHostSharedModules({
  '@perses-dev/spec': PersesSpec,
  '@perses-dev/client': PersesClient,
  '@perses-dev/components': PersesComponents,
  '@perses-dev/plugin-system': PersesPluginSystem,
  '@perses-dev/explore': PersesExplore,
  '@perses-dev/dashboards': PersesDashboards,
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

@perses-dev/components: removed hooks and stricter types#

To comply with the React Compiler rules, the following helpers are removed from @perses-dev/components:

  • useMemoized and useDeepMemo: use React's useMemo instead.
  • useId(prefix): use React's useId instead.
// Before
import { useId } from '@perses-dev/components';
const id = useId('MyComponent');

// After
import { useId } from 'react';
const id = `MyComponent-${useId()}`;

Some types are also stricter (any is replaced by unknown):

  • @perses-dev/client: the spec of a Resource is now unknown. Narrow it (or cast it to the expected type) before using it.
  • @perses-dev/components: the cell values of a TableColumnConfig are now unknown, and cellDescription now takes a CellContext<T, unknown>. In the cell callbacks, use getValue<ValueType>() when the value type is known, or narrow the value before using it.

@perses-dev/dashboards: annotation store refactor#

The annotation data is now fetched on demand through the TanStack Query cache (like the panel queries) instead of being loaded when the dashboard is opened and copied into the annotation store. As a consequence:

  • AnnotationHydrationWrapper is removed.
  • The annotationState field and the setAnnotationState action of the annotation store are removed.
  • useAnnotationsWithData, useAnnotationStates and useAnnotationSpecAndState now fetch the data themselves. They must be used under the QueryClientProvider, the plugin registry, the time range, the variable and the datasource store providers.
  • The TanStack Query key of the annotations changed from ['Annotation', ...] to ['annotation', ...]. If you invalidate the annotation queries yourself, use queryClient.invalidateQueries({ queryKey: ['annotation'] }).

@perses-dev/dashboards: panel repeat and grid layout#

To support the repetition of a single panel, the repeat variable identifying a repeated panel (repeatVariable in PanelGroupItemId and VirtualPanelRef) is no longer a [name, value] tuple. It's now an object distinguishing the variable repeating the panel group from the one repeating the panel:

// Before
setViewPanel({ panelGroupId, panelGroupItemLayoutId, repeatVariable: ['env', 'prod'] });
const [name, value] = panelGroupItemId.repeatVariable ?? [];

// After
setViewPanel({ panelGroupId, panelGroupItemLayoutId, repeatVariable: { group: ['env', 'prod'] } });
const [name, value] = panelGroupItemId.repeatVariable?.group ?? [];
// A repeated panel in a repeated panel group: { group: ['env', 'prod'], panel: ['host', 'a'] }

The viewPanelRef query parameter (the panel displayed in full screen) uses the same format. The links bookmarked or shared with v0.54 pointing to a panel in full screen inside a repeated panel group no longer work: they display an empty dashboard. Re-create them, or replace "repeatVariable":["env","prod"] by "repeatVariable":{"group":["env","prod"]} in the URL.

The grid layout is now based on Snapgrid (@snapgridjs/react) instead of react-grid-layout, to allow moving panels between panel groups:

  • Row (and the repeated grid layouts) no longer accept the props gridColWidth and onWidthChange, and onLayoutChange now receives the new layout of the panel group: (layout: PanelGroupItemLayout[]) => void.
  • The CSS classes of react-grid-layout (.react-grid-layout, .react-grid-item, .react-grid-placeholder, .react-resizable-handle) no longer exist. Use .snapgrid-item, .snapgrid-placeholder, .snapgrid-resize-handle--se and the attribute [data-grid-id] in your custom styles and E2E tests.

The dashboard store also has new required fields (e.g. repeatVariableMaxValues and setViewPanelFromRef): update the mocks of the store accordingly.

@perses-dev/plugin-system and @perses-dev/client: API changes#

  • PanelSpecEditor has two new required props, variableDefinitionGroups and onAnnotationsChange (also required by PanelQueriesSharedControls in @perses-dev/dashboards), and an optional panelGroups. Its new "Layout" tab relies on the react-hook-form context, so it must be rendered under a FormProvider:
// Before
<PanelSpecEditor control={form.control} panelDefinition={panelDefinition} ... />

// After
<FormProvider {...form}>
  <PanelSpecEditor
    control={form.control}
    panelDefinition={panelDefinition}
    variableDefinitionGroups={variableDefinitionGroups} // e.g. useAllVariableDefinitions() from @perses-dev/dashboards
    onAnnotationsChange={(annotations) => setAnnotations(annotations)}
    ...
  />
</FormProvider>
  • executeAction requires a new parameter fetchFn, used to call the webhooks:
import { useFetch } from '@perses-dev/client';

const { fetch } = useFetch();
await executeAction({ action, selectionMap, variableState, setActionStatus, fetchFn: fetch });
  • If you implement your own plugin registry, listPluginMetadata can now be called without any argument, in which case it must return the plugins of every type: listPluginMetadata(pluginTypes?: PluginType[]).
  • The value of TimeRangeSettingsContext has two new required fields, disableAutoRefresh and autoRefreshIntervalOptions. Use TimeRangeSettingsProvider rather than providing the context yourself.
  • When a remote plugin is loaded with baseURL: '', it's now resolved from the root of the site instead of /plugins. Don't set baseURL (or set it to '/plugins') to keep the previous behavior.
  • @perses-dev/client: the name of BuildDatasourceProxyUrlParams is now optional (it's omitted for a datasource that is not saved yet). A custom BuildDatasourceProxyUrlFunc must handle a name being undefined, or reuse buildProxyUrl from @perses-dev/client.
  • Some union types have new members, which breaks the exhaustive switch statements and Record types: PluginType includes 'JsonQuery', and Transform includes the PivotByLabel transform.

@perses-dev/explore: the default explorer must be provided#

ExploreManager no longer selects the first explorer when none is selected. If you compose the explore components yourself, pass the default explorer to the provider (ViewExplore already does it):

// Before
<ExplorerManagerProviderWithQueryParams>
  <ExploreManager />
</ExplorerManagerProviderWithQueryParams>

// After
import { getExplorerKey, useSortedExplorerPlugins } from '@perses-dev/explore';

const plugins = useSortedExplorerPlugins();
const defaultExplorer = plugins?.[0] ? getExplorerKey(plugins[0]) : undefined;

<ExplorerManagerProviderWithQueryParams defaultExplorer={defaultExplorer}>
  <ExploreManager />
</ExplorerManagerProviderWithQueryParams>

Official plugin packages: API changes#

If you import the npm packages of the official plugins:

  • Pyroscope: searchProfiles (and the types SearchProfilesParameters, SearchProfilesResponse, Flamebearer, Metadata, Timeline) and transformProfileResponse are removed. Use selectMergeStacktraces and selectSeries of PyroscopeClient, and transformFlameGraph / transformTimeline.
  • Pyroscope and Jaeger: executeRequest now takes the fetch function to use as first argument: executeRequest(url, init) becomes executeRequest(undefined, url, init).
  • Prometheus: the PrometheusClient interface has a new required method queryExemplars. Implement it in your custom clients and mocks.
  • TimeSeriesChart: TimeSeriesChartPaletteOptions is now a union type ({ mode: 'auto' } | { mode: 'categorical'; name?: string }), so an interface can no longer extend it.