diff --git a/docs/components/Dash0.mdx b/docs/components/Dash0.mdx
index faacc53df3..75e7bf8f54 100644
--- a/docs/components/Dash0.mdx
+++ b/docs/components/Dash0.mdx
@@ -11,6 +11,7 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
+
@@ -150,3 +151,48 @@ Returns the Prometheus query response including:
}
```
+
+
+## Update Check Rule
+
+The Update Check Rule component updates an existing Dash0 check rule.
+
+### Use Cases
+
+- **Threshold tuning**: Adjust alert sensitivity as service behavior changes
+- **Rule maintenance**: Update labels, annotations, and notification routing
+- **Operational automation**: Enable or disable rules from workflows
+
+### Configuration
+
+- **Check Rule**: Existing check rule origin/ID
+- **Name**: Human-readable rule name
+- **Expression**: Prometheus expression used by the rule
+- **For (Optional)**: How long expression must remain true before firing
+- **Interval (Optional)**: Evaluation interval override
+- **Keep Firing For (Optional)**: Additional duration to keep firing after recovery
+- **Labels (Optional)**: Label key/value pairs
+- **Annotations (Optional)**: Annotation key/value pairs
+
+### Output
+
+Emits:
+- **originOrId**: Check rule identifier used for the API request
+- **response**: Raw Dash0 API response
+
+### Example Output
+
+```json
+{
+ "data": {
+ "originOrId": "checkout-errors",
+ "response": {
+ "id": "checkout-errors",
+ "status": "updated"
+ }
+ },
+ "timestamp": "2026-02-09T12:00:00Z",
+ "type": "dash0.check.rule.updated"
+}
+```
+
diff --git a/pkg/integrations/dash0/check_rule.go b/pkg/integrations/dash0/check_rule.go
new file mode 100644
index 0000000000..ad3720edc9
--- /dev/null
+++ b/pkg/integrations/dash0/check_rule.go
@@ -0,0 +1,107 @@
+package dash0
+
+import (
+ "fmt"
+ "strings"
+)
+
+// CheckRuleKeyValue represents a single key/value entry for labels or annotations.
+type CheckRuleKeyValue struct {
+ Key string `json:"key" mapstructure:"key"`
+ Value string `json:"value" mapstructure:"value"`
+}
+
+// UpsertCheckRuleConfiguration contains user input for the check rule upsert actions.
+type UpsertCheckRuleConfiguration struct {
+ OriginOrID string `json:"originOrId" mapstructure:"originOrId"`
+ Name string `json:"name" mapstructure:"name"`
+ Expression string `json:"expression" mapstructure:"expression"`
+ For string `json:"for" mapstructure:"for"`
+ Interval string `json:"interval" mapstructure:"interval"`
+ KeepFiringFor string `json:"keepFiringFor" mapstructure:"keepFiringFor"`
+ Labels []CheckRuleKeyValue `json:"labels" mapstructure:"labels"`
+ Annotations []CheckRuleKeyValue `json:"annotations" mapstructure:"annotations"`
+}
+
+// buildCheckRuleSpecification validates and normalizes the check rule payload.
+func buildCheckRuleSpecification(config UpsertCheckRuleConfiguration, scope string) (map[string]any, error) {
+ ruleName, err := requireNonEmptyValue(config.Name, "name", scope)
+ if err != nil {
+ return nil, err
+ }
+
+ expression, err := requireNonEmptyValue(config.Expression, "expression", scope)
+ if err != nil {
+ return nil, err
+ }
+
+ specification := map[string]any{
+ "name": ruleName,
+ "expression": expression,
+ }
+
+ addOptionalStringField(specification, "for", config.For)
+ addOptionalStringField(specification, "interval", config.Interval)
+ addOptionalStringField(specification, "keepFiringFor", config.KeepFiringFor)
+
+ labels, err := normalizeKeyValuePairs(config.Labels, "labels", scope)
+ if err != nil {
+ return nil, err
+ }
+ if len(labels) > 0 {
+ specification["labels"] = labels
+ }
+
+ annotations, err := normalizeKeyValuePairs(config.Annotations, "annotations", scope)
+ if err != nil {
+ return nil, err
+ }
+ if len(annotations) > 0 {
+ specification["annotations"] = annotations
+ }
+
+ return specification, nil
+}
+
+// addOptionalStringField adds a field when the provided value is non-empty.
+func addOptionalStringField(target map[string]any, fieldName, value string) {
+ trimmed := strings.TrimSpace(value)
+ if trimmed == "" {
+ return
+ }
+
+ target[fieldName] = trimmed
+}
+
+// normalizeKeyValuePairs validates and normalizes list-based key/value entries.
+func normalizeKeyValuePairs(pairs []CheckRuleKeyValue, fieldName, scope string) (map[string]string, error) {
+ if len(pairs) == 0 {
+ return nil, nil
+ }
+
+ normalized := make(map[string]string, len(pairs))
+ for index, pair := range pairs {
+ key := strings.TrimSpace(pair.Key)
+ if key == "" {
+ return nil, fmt.Errorf("%s: %s[%d].key is required", scope, fieldName, index)
+ }
+
+ if _, exists := normalized[key]; exists {
+ return nil, fmt.Errorf("%s: %s[%d].key %q is duplicated", scope, fieldName, index, key)
+ }
+
+ normalized[key] = strings.TrimSpace(pair.Value)
+ }
+
+ return normalized, nil
+}
+
+// requireNonEmptyValue trims and validates required string values.
+func requireNonEmptyValue(value, fieldName, scope string) (string, error) {
+ trimmed := strings.TrimSpace(value)
+ if trimmed == "" {
+ return "", fmt.Errorf("%s: %s is required", scope, fieldName)
+ }
+
+ return trimmed, nil
+}
diff --git a/pkg/integrations/dash0/client.go b/pkg/integrations/dash0/client.go
index 7a16eb1baa..5566e6b2ff 100644
--- a/pkg/integrations/dash0/client.go
+++ b/pkg/integrations/dash0/client.go
@@ -1,6 +1,7 @@
package dash0
import (
+ "bytes"
"encoding/json"
"fmt"
"io"
@@ -86,6 +87,20 @@ func (c *Client) execRequest(method, url string, body io.Reader, contentType str
return responseBody, nil
}
+// withDefaultDatasetQuery appends dataset=default to Dash0 configuration API requests.
+func withDefaultDatasetQuery(requestURL string) (string, error) {
+ parsedURL, err := url.Parse(requestURL)
+ if err != nil {
+ return "", fmt.Errorf("error parsing request URL: %v", err)
+ }
+
+ query := parsedURL.Query()
+ query.Set("dataset", "default")
+ parsedURL.RawQuery = query.Encode()
+
+ return parsedURL.String(), nil
+}
+
type PrometheusResponse struct {
Status string `json:"status"`
Data PrometheusResponseData `json:"data"`
@@ -168,8 +183,12 @@ type CheckRule struct {
func (c *Client) ListCheckRules() ([]CheckRule, error) {
apiURL := fmt.Sprintf("%s/api/alerting/check-rules", c.BaseURL)
+ requestURL, err := withDefaultDatasetQuery(apiURL)
+ if err != nil {
+ return nil, fmt.Errorf("error setting check rules dataset query: %v", err)
+ }
- responseBody, err := c.execRequest(http.MethodGet, apiURL, nil, "")
+ responseBody, err := c.execRequest(http.MethodGet, requestURL, nil, "")
if err != nil {
return nil, err
}
@@ -207,3 +226,42 @@ func (c *Client) ListCheckRules() ([]CheckRule, error) {
return checkRules, nil
}
+
+// UpsertCheckRule creates or updates a Dash0 check rule by origin/ID.
+func (c *Client) UpsertCheckRule(originOrID string, specification map[string]any) (map[string]any, error) {
+ trimmedOriginOrID := strings.TrimSpace(originOrID)
+ if trimmedOriginOrID == "" {
+ return nil, fmt.Errorf("check rule originOrId is required")
+ }
+
+ if len(specification) == 0 {
+ return nil, fmt.Errorf("check rule specification is required")
+ }
+
+ requestBody, err := json.Marshal(specification)
+ if err != nil {
+ return nil, fmt.Errorf("error serializing check rule specification: %v", err)
+ }
+
+ apiURL := fmt.Sprintf("%s/api/alerting/check-rules/%s", c.BaseURL, url.PathEscape(trimmedOriginOrID))
+ requestURL, err := withDefaultDatasetQuery(apiURL)
+ if err != nil {
+ return nil, fmt.Errorf("error setting check rule dataset query: %v", err)
+ }
+
+ responseBody, err := c.execRequest(http.MethodPut, requestURL, bytes.NewReader(requestBody), "application/json")
+ if err != nil {
+ return nil, err
+ }
+
+ if len(responseBody) == 0 {
+ return map[string]any{}, nil
+ }
+
+ var response map[string]any
+ if err := json.Unmarshal(responseBody, &response); err != nil {
+ return nil, fmt.Errorf("error parsing check rule upsert response: %v", err)
+ }
+
+ return response, nil
+}
diff --git a/pkg/integrations/dash0/client_test.go b/pkg/integrations/dash0/client_test.go
index fd33e0c43a..f19908c3ec 100644
--- a/pkg/integrations/dash0/client_test.go
+++ b/pkg/integrations/dash0/client_test.go
@@ -210,3 +210,53 @@ func Test__Client__ExecutePrometheusRangeQuery(t *testing.T) {
assert.Contains(t, httpContext.Requests[0].URL.String(), "/api/prometheus/api/v1/query_range")
})
}
+
+func Test__Client__UpsertCheckRule(t *testing.T) {
+ t.Run("successful upsert", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(`{"status":"updated"}`)),
+ },
+ },
+ }
+
+ integrationCtx := &contexts.IntegrationContext{
+ Configuration: map[string]any{
+ "apiToken": "token123",
+ "baseURL": "https://api.us-west-2.aws.dash0.com",
+ },
+ }
+
+ client, err := NewClient(httpContext, integrationCtx)
+ require.NoError(t, err)
+
+ response, err := client.UpsertCheckRule("checkout-errors", map[string]any{
+ "name": "CheckoutErrors",
+ "expression": "sum(rate(http_requests_total[5m])) > 1",
+ })
+ require.NoError(t, err)
+ assert.Equal(t, "updated", response["status"])
+ require.Len(t, httpContext.Requests, 1)
+ assert.Equal(t, http.MethodPut, httpContext.Requests[0].Method)
+ assert.Contains(t, httpContext.Requests[0].URL.String(), "/api/alerting/check-rules/checkout-errors")
+ assert.Equal(t, "default", httpContext.Requests[0].URL.Query().Get("dataset"))
+ })
+
+ t.Run("missing origin id", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{}
+ integrationCtx := &contexts.IntegrationContext{
+ Configuration: map[string]any{
+ "apiToken": "token123",
+ "baseURL": "https://api.us-west-2.aws.dash0.com",
+ },
+ }
+
+ client, err := NewClient(httpContext, integrationCtx)
+ require.NoError(t, err)
+
+ _, err = client.UpsertCheckRule(" ", map[string]any{"name": "Rule", "expression": "up > 0"})
+ require.ErrorContains(t, err, "originOrId is required")
+ })
+}
diff --git a/pkg/integrations/dash0/dash0.go b/pkg/integrations/dash0/dash0.go
index a5f96699e3..e3d578fe40 100644
--- a/pkg/integrations/dash0/dash0.go
+++ b/pkg/integrations/dash0/dash0.go
@@ -69,6 +69,7 @@ func (d *Dash0) Components() []core.Component {
return []core.Component{
&QueryPrometheus{},
&ListIssues{},
+ &UpdateCheckRule{},
}
}
diff --git a/pkg/integrations/dash0/example.go b/pkg/integrations/dash0/example.go
index a2737e8e53..e69e7c35ba 100644
--- a/pkg/integrations/dash0/example.go
+++ b/pkg/integrations/dash0/example.go
@@ -19,6 +19,12 @@ var exampleOutputListIssuesBytes []byte
var exampleOutputListIssuesOnce sync.Once
var exampleOutputListIssues map[string]any
+//go:embed example_output_update_check_rule.json
+var exampleOutputUpdateCheckRuleBytes []byte
+
+var exampleOutputUpdateCheckRuleOnce sync.Once
+var exampleOutputUpdateCheckRule map[string]any
+
func (c *QueryPrometheus) ExampleOutput() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleOutputQueryPrometheusOnce, exampleOutputQueryPrometheusBytes, &exampleOutputQueryPrometheus)
}
@@ -26,3 +32,8 @@ func (c *QueryPrometheus) ExampleOutput() map[string]any {
func (c *ListIssues) ExampleOutput() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleOutputListIssuesOnce, exampleOutputListIssuesBytes, &exampleOutputListIssues)
}
+
+// ExampleOutput returns sample output data for Update Check Rule.
+func (c *UpdateCheckRule) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputUpdateCheckRuleOnce, exampleOutputUpdateCheckRuleBytes, &exampleOutputUpdateCheckRule)
+}
diff --git a/pkg/integrations/dash0/example_output_update_check_rule.json b/pkg/integrations/dash0/example_output_update_check_rule.json
new file mode 100644
index 0000000000..8daf797b6d
--- /dev/null
+++ b/pkg/integrations/dash0/example_output_update_check_rule.json
@@ -0,0 +1,11 @@
+{
+ "data": {
+ "originOrId": "checkout-errors",
+ "response": {
+ "id": "checkout-errors",
+ "status": "updated"
+ }
+ },
+ "timestamp": "2026-02-09T12:00:00Z",
+ "type": "dash0.check.rule.updated"
+}
diff --git a/pkg/integrations/dash0/update_check_rule.go b/pkg/integrations/dash0/update_check_rule.go
new file mode 100644
index 0000000000..0405b5b59a
--- /dev/null
+++ b/pkg/integrations/dash0/update_check_rule.go
@@ -0,0 +1,283 @@
+package dash0
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+)
+
+const UpdateCheckRulePayloadType = "dash0.check.rule.updated"
+
+// UpdateCheckRule updates existing Dash0 check rules via configuration API.
+type UpdateCheckRule struct{}
+
+// Name returns the stable component identifier.
+func (c *UpdateCheckRule) Name() string {
+ return "dash0.updateCheckRule"
+}
+
+// Label returns the display name used in the workflow builder.
+func (c *UpdateCheckRule) Label() string {
+ return "Update Check Rule"
+}
+
+// Description returns a short summary of component behavior.
+func (c *UpdateCheckRule) Description() string {
+ return "Update an existing check rule in Dash0 configuration API"
+}
+
+// Documentation returns markdown help shown in the component docs panel.
+func (c *UpdateCheckRule) Documentation() string {
+ return `The Update Check Rule component updates an existing Dash0 check rule.
+
+## Use Cases
+
+- **Threshold tuning**: Adjust alert sensitivity as service behavior changes
+- **Rule maintenance**: Update labels, annotations, and notification routing
+- **Operational automation**: Enable or disable rules from workflows
+
+## Configuration
+
+- **Check Rule**: Existing check rule origin/ID
+- **Name**: Human-readable rule name
+- **Expression**: Prometheus expression used by the rule
+- **For (Optional)**: How long expression must remain true before firing
+- **Interval (Optional)**: Evaluation interval override
+- **Keep Firing For (Optional)**: Additional duration to keep firing after recovery
+- **Labels (Optional)**: Label key/value pairs
+- **Annotations (Optional)**: Annotation key/value pairs
+
+## Output
+
+Emits:
+- **originOrId**: Check rule identifier used for the API request
+- **response**: Raw Dash0 API response`
+}
+
+// Icon returns the Lucide icon name for this component.
+func (c *UpdateCheckRule) Icon() string {
+ return "refresh-cw"
+}
+
+// Color returns the node color used in the UI.
+func (c *UpdateCheckRule) Color() string {
+ return "blue"
+}
+
+// OutputChannels declares the channel emitted by this action.
+func (c *UpdateCheckRule) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+// Configuration defines fields required to update check rules.
+func (c *UpdateCheckRule) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "originOrId",
+ Label: "Check Rule",
+ Type: configuration.FieldTypeIntegrationResource,
+ Required: true,
+ Togglable: true,
+ TypeOptions: &configuration.TypeOptions{
+ Resource: &configuration.ResourceTypeOptions{
+ Type: "check-rule",
+ },
+ },
+ Description: "Check rule origin/ID to update",
+ },
+ {
+ Name: "name",
+ Label: "Rule Name",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Description: "Name of the check rule",
+ Placeholder: "Checkout errors",
+ },
+ {
+ Name: "expression",
+ Label: "Expression",
+ Type: configuration.FieldTypeText,
+ Required: true,
+ Description: "Prometheus expression evaluated by the rule",
+ Placeholder: "sum(rate(http_requests_total{service=\"checkout\",status=~\"5..\"}[5m])) > 1",
+ },
+ {
+ Name: "for",
+ Label: "For",
+ Type: configuration.FieldTypeString,
+ Required: false,
+ Togglable: true,
+ Description: "Optional firing delay duration (for example: 5m)",
+ Placeholder: "5m",
+ },
+ {
+ Name: "interval",
+ Label: "Interval",
+ Type: configuration.FieldTypeString,
+ Required: false,
+ Togglable: true,
+ Description: "Optional evaluation interval override (for example: 1m)",
+ Placeholder: "1m",
+ },
+ {
+ Name: "keepFiringFor",
+ Label: "Keep Firing For",
+ Type: configuration.FieldTypeString,
+ Required: false,
+ Togglable: true,
+ Description: "Optional extra duration to keep alert firing after recovery",
+ Placeholder: "10m",
+ },
+ {
+ Name: "labels",
+ Label: "Labels",
+ Type: configuration.FieldTypeList,
+ Required: false,
+ Togglable: true,
+ Description: "Optional label key/value pairs",
+ TypeOptions: &configuration.TypeOptions{
+ List: &configuration.ListTypeOptions{
+ ItemLabel: "Label",
+ ItemDefinition: &configuration.ListItemDefinition{
+ Type: configuration.FieldTypeObject,
+ Schema: []configuration.Field{
+ {
+ Name: "key",
+ Label: "Key",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ DisallowExpression: true,
+ },
+ {
+ Name: "value",
+ Label: "Value",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "annotations",
+ Label: "Annotations",
+ Type: configuration.FieldTypeList,
+ Required: false,
+ Togglable: true,
+ Description: "Optional annotation key/value pairs",
+ TypeOptions: &configuration.TypeOptions{
+ List: &configuration.ListTypeOptions{
+ ItemLabel: "Annotation",
+ ItemDefinition: &configuration.ListItemDefinition{
+ Type: configuration.FieldTypeObject,
+ Schema: []configuration.Field{
+ {
+ Name: "key",
+ Label: "Key",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ DisallowExpression: true,
+ },
+ {
+ Name: "value",
+ Label: "Value",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+// Setup validates component configuration during save/setup.
+func (c *UpdateCheckRule) Setup(ctx core.SetupContext) error {
+ scope := "dash0.updateCheckRule setup"
+ config := UpsertCheckRuleConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("%s: decode configuration: %w", scope, err)
+ }
+
+ if _, err := requireNonEmptyValue(config.OriginOrID, "originOrId", scope); err != nil {
+ return err
+ }
+
+ if _, err := buildCheckRuleSpecification(config, scope); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ProcessQueueItem delegates queue processing to default behavior.
+func (c *UpdateCheckRule) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+// Execute updates a check rule and emits API response payload.
+func (c *UpdateCheckRule) Execute(ctx core.ExecutionContext) error {
+ scope := "dash0.updateCheckRule execute"
+ config := UpsertCheckRuleConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("%s: decode configuration: %w", scope, err)
+ }
+
+ originOrID, err := requireNonEmptyValue(config.OriginOrID, "originOrId", scope)
+ if err != nil {
+ return err
+ }
+
+ specification, err := buildCheckRuleSpecification(config, scope)
+ if err != nil {
+ return err
+ }
+
+ client, err := NewClient(ctx.HTTP, ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("%s: create client: %w", scope, err)
+ }
+
+ response, err := client.UpsertCheckRule(originOrID, specification)
+ if err != nil {
+ return fmt.Errorf("%s: update check rule %q: %w", scope, originOrID, err)
+ }
+
+ payload := map[string]any{
+ "originOrId": originOrID,
+ "response": response,
+ }
+
+ return ctx.ExecutionState.Emit(core.DefaultOutputChannel.Name, UpdateCheckRulePayloadType, []any{payload})
+}
+
+// Actions returns no manual actions for this component.
+func (c *UpdateCheckRule) Actions() []core.Action {
+ return []core.Action{}
+}
+
+// HandleAction is unused because this component has no actions.
+func (c *UpdateCheckRule) HandleAction(ctx core.ActionContext) error {
+ return nil
+}
+
+// HandleWebhook is unused because this component does not receive webhooks.
+func (c *UpdateCheckRule) HandleWebhook(ctx core.WebhookRequestContext) (int, error) {
+ return http.StatusOK, nil
+}
+
+// Cancel is a no-op because execution is synchronous and short-lived.
+func (c *UpdateCheckRule) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+// Cleanup is a no-op because no external resources are provisioned.
+func (c *UpdateCheckRule) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
diff --git a/pkg/integrations/dash0/update_check_rule_test.go b/pkg/integrations/dash0/update_check_rule_test.go
new file mode 100644
index 0000000000..a6d43904d9
--- /dev/null
+++ b/pkg/integrations/dash0/update_check_rule_test.go
@@ -0,0 +1,114 @@
+package dash0
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func Test__UpdateCheckRule__Setup(t *testing.T) {
+ component := UpdateCheckRule{}
+
+ t.Run("origin is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "originOrId": "",
+ "name": "Checkout errors",
+ "expression": `sum(rate(http_requests_total{service="checkout",status=~"5.."}[5m])) > 1`,
+ },
+ })
+
+ require.ErrorContains(t, err, "originOrId is required")
+ })
+
+ t.Run("name is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "originOrId": "checkout-errors",
+ "expression": `sum(rate(http_requests_total{service="checkout",status=~"5.."}[5m])) > 1`,
+ },
+ })
+
+ require.ErrorContains(t, err, "name is required")
+ })
+
+ t.Run("duplicate label keys are rejected", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "originOrId": "checkout-errors",
+ "name": "Checkout errors",
+ "expression": `sum(rate(http_requests_total{service="checkout",status=~"5.."}[5m])) > 1`,
+ "labels": []map[string]any{
+ {"key": "severity", "value": "critical"},
+ {"key": "severity", "value": "warning"},
+ },
+ },
+ })
+
+ require.ErrorContains(t, err, `labels[1].key "severity" is duplicated`)
+ })
+}
+
+func Test__UpdateCheckRule__Execute(t *testing.T) {
+ component := UpdateCheckRule{}
+
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(`{"status":"updated"}`)),
+ },
+ },
+ }
+
+ execCtx := &contexts.ExecutionStateContext{}
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "originOrId": "checkout-errors",
+ "name": "CheckoutErrors",
+ "expression": `sum(rate(http_requests_total{service="checkout",status=~"5.."}[5m])) > 1`,
+ "for": "10m",
+ "interval": "1m",
+ "labels": []map[string]any{
+ {"key": "severity", "value": "critical"},
+ },
+ "annotations": []map[string]any{
+ {"key": "summary", "value": "Checkout errors are critical"},
+ },
+ },
+ HTTP: httpContext,
+ Integration: &contexts.IntegrationContext{
+ Configuration: map[string]any{
+ "apiToken": "token123",
+ "baseURL": "https://api.us-west-2.aws.dash0.com",
+ },
+ },
+ ExecutionState: execCtx,
+ })
+
+ require.NoError(t, err)
+ assert.Equal(t, UpdateCheckRulePayloadType, execCtx.Type)
+ require.Len(t, httpContext.Requests, 1)
+ assert.Equal(t, http.MethodPut, httpContext.Requests[0].Method)
+ assert.Contains(t, httpContext.Requests[0].URL.String(), "/api/alerting/check-rules/checkout-errors")
+ assert.Equal(t, "default", httpContext.Requests[0].URL.Query().Get("dataset"))
+
+ requestBody, readErr := io.ReadAll(httpContext.Requests[0].Body)
+ require.NoError(t, readErr)
+
+ var payload map[string]any
+ require.NoError(t, json.Unmarshal(requestBody, &payload))
+ assert.Equal(t, "CheckoutErrors", payload["name"])
+ assert.Equal(t, "sum(rate(http_requests_total{service=\"checkout\",status=~\"5..\"}[5m])) > 1", payload["expression"])
+ assert.Equal(t, "10m", payload["for"])
+ assert.Equal(t, "1m", payload["interval"])
+ assert.Equal(t, map[string]any{"severity": "critical"}, payload["labels"])
+ assert.Equal(t, map[string]any{"summary": "Checkout errors are critical"}, payload["annotations"])
+}
diff --git a/web_src/src/pages/workflowv2/mappers/dash0/index.ts b/web_src/src/pages/workflowv2/mappers/dash0/index.ts
index fd9865aafd..79e9ae0e2d 100644
--- a/web_src/src/pages/workflowv2/mappers/dash0/index.ts
+++ b/web_src/src/pages/workflowv2/mappers/dash0/index.ts
@@ -4,10 +4,12 @@ import { withOrganizationHeader } from "@/utils/withOrganizationHeader";
import { queryPrometheusMapper } from "./query_prometheus";
import { listIssuesMapper, LIST_ISSUES_STATE_REGISTRY } from "./list_issues";
import { buildActionStateRegistry } from "../utils";
+import { updateCheckRuleMapper } from "./update_check_rule";
export const componentMappers: Record = {
queryPrometheus: queryPrometheusMapper,
listIssues: listIssuesMapper,
+ updateCheckRule: updateCheckRuleMapper,
};
export const triggerRenderers: Record = {};
@@ -15,6 +17,7 @@ export const triggerRenderers: Record = {};
export const eventStateRegistry: Record = {
listIssues: LIST_ISSUES_STATE_REGISTRY,
queryPrometheus: buildActionStateRegistry("queried"),
+ updateCheckRule: buildActionStateRegistry("updated"),
};
export async function resolveExecutionErrors(canvasId: string, executionIds: string[]) {
diff --git a/web_src/src/pages/workflowv2/mappers/dash0/types.ts b/web_src/src/pages/workflowv2/mappers/dash0/types.ts
index f26ca938e8..ebf6ec4ede 100644
--- a/web_src/src/pages/workflowv2/mappers/dash0/types.ts
+++ b/web_src/src/pages/workflowv2/mappers/dash0/types.ts
@@ -15,6 +15,23 @@ export interface ListIssuesConfiguration {
checkRules?: string[];
}
+export interface CheckRuleKeyValueConfiguration {
+ key: string;
+ value: string;
+}
+
+export interface UpsertCheckRuleConfiguration {
+ originOrId?: string;
+ name?: string;
+ expression?: string;
+ for?: string;
+ interval?: string;
+ keepFiringFor?: string;
+ labels?: CheckRuleKeyValueConfiguration[];
+ annotations?: CheckRuleKeyValueConfiguration[];
+ spec?: string;
+}
+
export interface PrometheusResponse {
status: string;
data: {
diff --git a/web_src/src/pages/workflowv2/mappers/dash0/update_check_rule.ts b/web_src/src/pages/workflowv2/mappers/dash0/update_check_rule.ts
new file mode 100644
index 0000000000..c5a4cf2296
--- /dev/null
+++ b/web_src/src/pages/workflowv2/mappers/dash0/update_check_rule.ts
@@ -0,0 +1,93 @@
+import { ComponentBaseProps, EventSection } from "@/ui/componentBase";
+import { getState, getStateMap, getTriggerRenderer } from "..";
+import {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../types";
+import dash0Icon from "@/assets/icons/integrations/dash0.svg";
+import { MetadataItem } from "@/ui/metadataList";
+import { formatTimeAgo } from "@/utils/date";
+import { UpsertCheckRuleConfiguration } from "./types";
+
+export const updateCheckRuleMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ const lastExecution = context.lastExecutions.length > 0 ? context.lastExecutions[0] : null;
+ const componentName = context.componentDefinition.name || "unknown";
+
+ return {
+ iconSrc: dash0Icon,
+ collapsedBackground: "bg-white",
+ collapsed: context.node.isCollapsed,
+ title: context.node.name || context.componentDefinition.label || "Update Check Rule",
+ eventSections: lastExecution ? baseEventSections(context.nodes, lastExecution, componentName) : undefined,
+ metadata: metadataList(context.node),
+ includeEmptyState: !lastExecution,
+ eventStateMap: getStateMap(componentName),
+ };
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default?: OutputPayload[] } | undefined;
+ if (!outputs || !outputs.default || outputs.default.length === 0) {
+ return { Response: "No data returned" };
+ }
+
+ const payload = outputs.default[0];
+ const responseData = payload?.data as Record | undefined;
+ if (!responseData) {
+ return { Response: "No data returned" };
+ }
+
+ const details: Record = {};
+ if (payload?.timestamp) {
+ details["Updated At"] = new Date(payload.timestamp).toLocaleString();
+ }
+
+ try {
+ details["Update Response"] = JSON.stringify(responseData, null, 2);
+ } catch {
+ details["Update Response"] = String(responseData);
+ }
+
+ return details;
+ },
+
+ subtitle(context: SubtitleContext): string {
+ return formatTimeAgo(new Date(context.execution.createdAt!));
+ },
+};
+
+function metadataList(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const configuration = node.configuration as UpsertCheckRuleConfiguration;
+
+ if (configuration?.originOrId) {
+ metadata.push({
+ icon: "hash",
+ label: configuration.originOrId,
+ });
+ }
+
+ return metadata;
+}
+
+function baseEventSections(nodes: NodeInfo[], execution: ExecutionInfo, componentName: string): EventSection[] {
+ const rootTriggerNode = nodes.find((node) => node.id === execution.rootEvent?.nodeId);
+ const rootTriggerRenderer = getTriggerRenderer(rootTriggerNode?.componentName || "");
+ const { title } = rootTriggerRenderer.getTitleAndSubtitle({ event: execution.rootEvent });
+
+ return [
+ {
+ receivedAt: new Date(execution.createdAt!),
+ eventTitle: title,
+ eventSubtitle: formatTimeAgo(new Date(execution.createdAt!)),
+ eventState: getState(componentName)(execution),
+ eventId: execution.rootEvent!.id!,
+ },
+ ];
+}